cdts/xdts-ios 3/TreeHole/CYHResetCode/CYH/QMUIKit/QMUIComponents/QMUITextView.m

487 lines
23 KiB
Mathematica
Raw Normal View History

2023-07-27 09:20:00 +08:00
/**
* Tencent is pleased to support the open source community by making QMUI_iOS available.
* Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
//
// QMUITextView.m
// qmui
//
// Created by QMUI Team on 14-8-5.
//
#import "QMUITextView.h"
#import "QMUICore.h"
#import "QMUILabel.h"
#import "NSObject+QMUI.h"
#import "NSString+QMUI.h"
#import "UITextView+QMUI.h"
#import "UIScrollView+QMUI.h"
#import "QMUILog.h"
#import "QMUIMultipleDelegates.h"
/// textView placeholder
const CGFloat kSystemTextViewDefaultFontPointSize = 12.0f;
/// textView.textContainerInset UIEdgeInsetsZero textView font1312y-1px
const UIEdgeInsets kSystemTextViewFixTextInsets = {0, 5, 0, 5};
// QMUITextViewDelegate self.delegate = self QMUITextView delegate
@interface _QMUITextViewDelegator : NSObject <QMUITextViewDelegate>
@property(nonatomic, weak) QMUITextView *textView;
@end
@interface QMUITextView ()
@property(nonatomic, assign) BOOL debug;
@property(nonatomic, assign) BOOL postInitializationMethodCalled;
@property(nonatomic, strong) _QMUITextViewDelegator *delegator;
@property(nonatomic, assign) BOOL isSettingTextByShouldChange;
@property(nonatomic, assign) BOOL shouldRejectSystemScroll;// handleTextChanged: contentOffset setContentOffset:
@property(nonatomic, strong) UILabel *placeholderLabel;
@end
@implementation QMUITextView
@dynamic delegate;
- (instancetype)initWithFrame:(CGRect)frame textContainer:(NSTextContainer *)textContainer {
if (self = [super initWithFrame:frame textContainer:textContainer]) {
[self didInitialize];
if (QMUICMIActivated) {
UIColor *textColor = TextFieldTextColor;
if (textColor) {
self.textColor = textColor;
}
self.tintColor = TextFieldTintColor;
}
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super initWithCoder:aDecoder]) {
[self didInitialize];
}
return self;
}
- (void)didInitialize {
self.debug = NO;
self.qmui_multipleDelegatesEnabled = YES;
self.delegator = [[_QMUITextViewDelegator alloc] init];
self.delegator.textView = self;
self.delegate = self.delegator;
self.scrollsToTop = NO;
if (QMUICMIActivated) self.placeholderColor = UIColorPlaceholder;
self.placeholderMargins = UIEdgeInsetsZero;
self.maximumHeight = CGFLOAT_MAX;
self.maximumTextLength = NSUIntegerMax;
self.shouldResponseToProgrammaticallyTextChanges = YES;
self.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
self.placeholderLabel = [[UILabel alloc] init];
self.placeholderLabel.font = UIFontMake(kSystemTextViewDefaultFontPointSize);
self.placeholderLabel.textColor = self.placeholderColor;
self.placeholderLabel.numberOfLines = 0;
self.placeholderLabel.alpha = 0;
[self addSubview:self.placeholderLabel];
// setText:
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleTextChanged:) name:UITextViewTextDidChangeNotification object:nil];
self.postInitializationMethodCalled = YES;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
self.delegate = nil;
}
- (NSString *)description {
return [NSString stringWithFormat:@"%@; text.length: %@ | %@; markedTextRange: %@", [super description], @(self.text.length), @([self lengthWithString:self.text]), self.markedTextRange];
}
- (BOOL)isCurrentTextDifferentOfText:(NSString *)text {
NSString *textBeforeChange = self.text;// UITextView self.text @"" nil便 nil get
if ([textBeforeChange isEqualToString:text] || (textBeforeChange.length == 0 && !text)) {
return NO;
}
return YES;
}
- (void)_qmui_setTextForShouldChange:(NSString *)text {
self.isSettingTextByShouldChange = YES;
[self setText:text];
// shouldResponseToProgrammaticallyTextChanges = YES textViewDidChange: self setText: shouldResponseToProgrammaticallyTextChanges = NO
if (!self.shouldResponseToProgrammaticallyTextChanges && [self.delegate respondsToSelector:@selector(textViewDidChange:)]) {
[self.delegate textViewDidChange:self];
}
self.isSettingTextByShouldChange = NO;
}
- (void)setAttributedText:(NSAttributedString *)attributedText {
BOOL textDifferent = [self isCurrentTextDifferentOfText:attributedText.string];
if (!textDifferent) {
[super setAttributedText:attributedText];
return;
}
if (self.shouldResponseToProgrammaticallyTextChanges) {
if (!self.isSettingTextByShouldChange) {
BOOL shouldChangeText = YES;
if ([self.delegate respondsToSelector:@selector(textView:shouldChangeTextInRange:replacementText:)]) {
NSString *textBeforeChange = self.attributedText.string;
shouldChangeText = [self.delegate textView:self shouldChangeTextInRange:NSMakeRange(0, textBeforeChange.length) replacementText:attributedText.string];
}
if (!shouldChangeText) {
return;
}
}
[super setAttributedText:attributedText];
if ([self.delegate respondsToSelector:@selector(textViewDidChange:)]) {
[self.delegate textViewDidChange:self];
}
} else {
[super setAttributedText:attributedText];
}
[self handleTextChanged:self];
}
- (void)setTypingAttributes:(NSDictionary<NSString *,id> *)typingAttributes {
[super setTypingAttributes:typingAttributes];
[self updatePlaceholderStyle];
}
- (void)setFont:(UIFont *)font {
[super setFont:font];
[self updatePlaceholderStyle];
}
- (void)setTextColor:(UIColor *)textColor {
[super setTextColor:textColor];
[self updatePlaceholderStyle];
}
- (void)setTextAlignment:(NSTextAlignment)textAlignment {
[super setTextAlignment:textAlignment];
[self updatePlaceholderStyle];
}
- (void)setPlaceholder:(NSString *)placeholder {
_placeholder = placeholder;
self.placeholderLabel.attributedText = placeholder ? [[NSAttributedString alloc] initWithString:_placeholder attributes:self.typingAttributes] : nil;
if (self.placeholderColor) {
self.placeholderLabel.textColor = self.placeholderColor;
}
[self sendSubviewToBack:self.placeholderLabel];
[self setNeedsLayout];
[self updatePlaceholderLabelHidden];
}
- (void)setPlaceholderColor:(UIColor *)placeholderColor {
_placeholderColor = placeholderColor;
self.placeholderLabel.textColor = _placeholderColor;
}
- (void)updatePlaceholderStyle {
self.placeholder = self.placeholder;//
}
- (void)updatePlaceholderLabelHidden {
if (self.text.length == 0 && self.placeholder.length > 0) {
self.placeholderLabel.alpha = 1;
} else {
self.placeholderLabel.alpha = 0;// alphaplaceholder placeholder layout
}
}
- (CGRect)preferredPlaceholderFrameWithSize:(CGSize)size {
if (self.placeholder.length <= 0) return CGRectZero;
UIEdgeInsets allInsets = self.allInsets;
UIEdgeInsets labelMargins = UIEdgeInsetsMake(allInsets.top - self.adjustedContentInset.top, allInsets.left - self.adjustedContentInset.left, allInsets.bottom - self.adjustedContentInset.bottom, allInsets.right - self.adjustedContentInset.right);
CGFloat limitWidth = size.width - UIEdgeInsetsGetHorizontalValue(allInsets);
CGFloat limitHeight = size.height - UIEdgeInsetsGetVerticalValue(allInsets);
CGSize labelSize = [self.placeholderLabel sizeThatFits:CGSizeMake(limitWidth, limitHeight)];
labelSize.width = limitWidth == CGFLOAT_MAX ? MIN(limitWidth, labelSize.width) : limitWidth;// limitWidth CGFLOAT_MAX sizeToFit sizeThatFits: placeholder labelSize.width placeholderLabel NSTextAlignmentRight
labelSize.height = MIN(limitHeight, labelSize.height);
return CGRectFlatMake(labelMargins.left, labelMargins.top, labelSize.width, labelSize.height);
}
- (void)handleTextChanged:(id)sender {
QMUITextView *textView = nil;
if ([sender isKindOfClass:[NSNotification class]]) {
id object = ((NSNotification *)sender).object;
if (object == self) {
textView = (QMUITextView *)object;
}
} else if ([sender isKindOfClass:[QMUITextView class]]) {
textView = (QMUITextView *)sender;
}
if (!textView) return;
// placeholder
if (self.placeholder.length > 0) {
[self updatePlaceholderLabelHidden];
}
// crash
// https://github.com/Tencent/QMUI_iOS/issues/1168
if (textView.maximumTextLength < NSUIntegerMax && (textView.undoManager.undoing || textView.undoManager.redoing)) {
return;
}
// textView:shouldChangeTextInRange:replacementText: handleTextChanged: handleTextChanged:
if (!textView.markedTextRange && [textView lengthWithString:textView.text] > textView.maximumTextLength) {
NSString *finalText = [textView.text qmui_substringAvoidBreakingUpCharacterSequencesWithRange:NSMakeRange(0, textView.maximumTextLength) lessValue:YES countingNonASCIICharacterAsTwo:textView.shouldCountingNonASCIICharacterAsTwo];
NSString *replacementText = [textView.text substringFromIndex:finalText.length];
textView.text = finalText;
if ([textView.delegate respondsToSelector:@selector(textView:didPreventTextChangeInRange:replacementText:)]) {
// selectedRange replacementText nil
[textView.delegate textView:textView didPreventTextChangeInRange:textView.selectedRange replacementText:replacementText];
}
}
if (!textView.editable) {
return;// textView
}
//
if ([textView.delegate respondsToSelector:@selector(textView:newHeightAfterTextChanged:)]) {
CGFloat resultHeight = flat([textView sizeThatFits:CGSizeMake(CGRectGetWidth(textView.bounds), CGFLOAT_MAX)].height);
if (textView.debug) QMUILog(NSStringFromClass(textView.class), @"handleTextDidChange, text = %@, resultHeight = %f", textView.text, resultHeight);
// delegatetextView
if (resultHeight != flat(CGRectGetHeight(textView.bounds))) {
[textView.delegate textView:textView newHeightAfterTextChanged:resultHeight];
}
}
// textView
if (!textView.window) {
return;
}
textView.shouldRejectSystemScroll = YES;
// dispatch
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
textView.shouldRejectSystemScroll = NO;
[textView qmui_scrollCaretVisibleAnimated:YES];
});
}
- (CGSize)sizeThatFits:(CGSize)size {
if (size.width <= 0) size.width = CGFLOAT_MAX;
if (size.height <= 0) size.height = CGFLOAT_MAX;
CGSize result = CGSizeZero;
if (self.placeholder.length > 0 && self.text.length <= 0) {
UIEdgeInsets allInsets = self.allInsets;
CGRect frame = [self preferredPlaceholderFrameWithSize:size];
result.width = CGRectGetWidth(frame) + UIEdgeInsetsGetHorizontalValue(allInsets);
result.height = CGRectGetHeight(frame) + UIEdgeInsetsGetVerticalValue(allInsets);
} else {
result = [super sizeThatFits:size];
}
result.height = MIN(result.height, self.maximumHeight);
return result;
}
- (UIEdgeInsets)allInsets {
return UIEdgeInsetsConcat(UIEdgeInsetsConcat(UIEdgeInsetsConcat(self.textContainerInset, self.placeholderMargins), kSystemTextViewFixTextInsets), self.adjustedContentInset);
}
- (void)setFrame:(CGRect)frame {
if (self.postInitializationMethodCalled) {
// didInitialize self.maximumHeight CGFLOAT_MAX 0 initWithFrame: 0
frame = CGRectSetHeight(frame, MIN(CGRectGetHeight(frame), self.maximumHeight));
}
// UITextView drawRect: frame 线
// https://github.com/Tencent/QMUI_iOS/issues/557
frame = CGRectFlatted(frame);
// UITextView setFrame: rect setContentOffset
BOOL sizeChanged = !CGSizeEqualToSize(frame.size, self.frame.size);
if (!sizeChanged) {
self.shouldRejectSystemScroll = YES;
}
[super setFrame:frame];
if (!sizeChanged) {
self.shouldRejectSystemScroll = NO;
}
}
- (void)setBounds:(CGRect)bounds {
// UITextView drawRect: frame 线
// https://github.com/Tencent/QMUI_iOS/issues/557
bounds = CGRectFlatted(bounds);
[super setBounds:bounds];
}
- (void)layoutSubviews {
[super layoutSubviews];
if (self.placeholder.length > 0) {
CGRect frame = [self preferredPlaceholderFrameWithSize:self.bounds.size];
self.placeholderLabel.frame = frame;
}
}
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
[self updatePlaceholderLabelHidden];
}
- (NSUInteger)lengthWithString:(NSString *)string {
return self.shouldCountingNonASCIICharacterAsTwo ? string.qmui_lengthWhenCountingNonASCIICharacterAsTwo : string.length;
}
- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated {
if (!self.shouldRejectSystemScroll) {
[super setContentOffset:contentOffset animated:animated];
if (self.debug) QMUILog(NSStringFromClass(self.class), @"%@, contentOffset.y = %.2f", NSStringFromSelector(_cmd), contentOffset.y);
} else {
if (self.debug) QMUILog(NSStringFromClass(self.class), @"被屏蔽的 %@, contentOffset.y = %.2f", NSStringFromSelector(_cmd), contentOffset.y);
}
}
- (void)setContentOffset:(CGPoint)contentOffset {
if (!self.shouldRejectSystemScroll) {
[super setContentOffset:contentOffset];
if (self.debug) QMUILog(NSStringFromClass(self.class), @"%@, contentOffset.y = %.2f", NSStringFromSelector(_cmd), contentOffset.y);
} else {
if (self.debug) QMUILog(NSStringFromClass(self.class), @"被屏蔽的 %@, contentOffset.y = %.2f", NSStringFromSelector(_cmd), contentOffset.y);
}
}
#pragma mark - <UIResponderStandardEditActions>
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
BOOL superReturnValue = [super canPerformAction:action withSender:sender];
if (action == @selector(paste:) && self.canPerformPasteActionBlock) {
return self.canPerformPasteActionBlock(sender, superReturnValue);
}
return superReturnValue;
}
- (void)paste:(id)sender {
BOOL shouldCallSuper = YES;
if (self.pasteBlock) {
shouldCallSuper = self.pasteBlock(sender);
}
if (shouldCallSuper) {
[super paste:sender];
}
}
@end
@implementation _QMUITextViewDelegator
#pragma mark - <QMUITextViewDelegate>
- (BOOL)textView:(QMUITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
if (self.textView.debug) QMUILog(NSStringFromClass(self.class), @"textView.text(%@ | %@) = %@\nmarkedTextRange = %@\nrange = %@\ntext = %@", @(textView.text.length), @(textView.text.qmui_lengthWhenCountingNonASCIICharacterAsTwo), textView.text, textView.markedTextRange, NSStringFromRange(range), text);
if ([text isEqualToString:@"\n"]) {
if ([textView.delegate respondsToSelector:@selector(textViewShouldReturn:)]) {
BOOL shouldReturn = [textView.delegate textViewShouldReturn:textView];
if (shouldReturn) {
return NO;
}
}
}
if (textView.maximumTextLength < NSUIntegerMax) {
// markedTextRange nilhuang5
// textView:shouldChangeTextInRange:replacementText: marktedTextRange handleTextChanged:
if (textView.markedTextRange) {
if ([textView.delegate respondsToSelector:@selector(textView:shouldChangeTextInRange:replacementText:originalValue:)]) {
return [textView.delegate textView:textView shouldChangeTextInRange:range replacementText:text originalValue:YES];
}
return YES;
}
if (NSMaxRange(range) > textView.text.length) {
// range YES rash
// https://github.com/Tencent/QMUI_iOS/issues/377
// https://github.com/Tencent/QMUI_iOS/issues/1170
// NO range
range = NSMakeRange(range.location, range.length - (NSMaxRange(range) - textView.text.length));
if (range.length > 0) {
UITextRange *textRange = [self.textView qmui_convertUITextRangeFromNSRange:range];
[self.textView replaceRange:textRange withText:text];
}
return NO;
}
if (!text.length && range.length > 0) {
// #377#1170
if ([textView.delegate respondsToSelector:@selector(textView:shouldChangeTextInRange:replacementText:originalValue:)]) {
return [textView.delegate textView:textView shouldChangeTextInRange:range replacementText:text originalValue:YES];
}
return YES;
}
NSUInteger rangeLength = textView.shouldCountingNonASCIICharacterAsTwo ? [textView.text substringWithRange:range].qmui_lengthWhenCountingNonASCIICharacterAsTwo : range.length;
BOOL textWillOutofMaximumTextLength = [textView lengthWithString:textView.text] - rangeLength + [textView lengthWithString:text] > textView.maximumTextLength;
if (textWillOutofMaximumTextLength) {
// return \n return return
if ([textView lengthWithString:textView.text] - rangeLength == textView.maximumTextLength && [text isEqualToString:@"\n"]) {
return NO;
}
//
NSInteger substringLength = textView.maximumTextLength - [textView lengthWithString:textView.text] + rangeLength;
if (substringLength > 0 && [textView lengthWithString:text] > substringLength) {
NSString *allowedText = [text qmui_substringAvoidBreakingUpCharacterSequencesWithRange:NSMakeRange(0, substringLength) lessValue:YES countingNonASCIICharacterAsTwo:textView.shouldCountingNonASCIICharacterAsTwo];
if ([textView lengthWithString:allowedText] <= substringLength) {
BOOL shouldChange = YES;
if ([textView.delegate respondsToSelector:@selector(textView:shouldChangeTextInRange:replacementText:originalValue:)]) {
shouldChange = [textView.delegate textView:textView shouldChangeTextInRange:range replacementText:text originalValue:YES];
}
if (!shouldChange) {
return NO;
}
[textView _qmui_setTextForShouldChange:[textView.text stringByReplacingCharactersInRange:range withString:allowedText]];
// iOS 10 selectedRange iOS 11
NSRange finalSelectedRange = NSMakeRange(range.location + substringLength, 0);
textView.selectedRange = finalSelectedRange;
dispatch_async(dispatch_get_main_queue(), ^{
textView.selectedRange = finalSelectedRange;
});
}
}
if ([textView.delegate respondsToSelector:@selector(textView:didPreventTextChangeInRange:replacementText:)]) {
[textView.delegate textView:textView didPreventTextChangeInRange:range replacementText:text];
}
return NO;
}
}
if ([textView.delegate respondsToSelector:@selector(textView:shouldChangeTextInRange:replacementText:originalValue:)]) {
return [textView.delegate textView:textView shouldChangeTextInRange:range replacementText:text originalValue:YES];
}
return YES;
}
@end