-
Notifications
You must be signed in to change notification settings - Fork 290
/
Copy pathSPTextView.m
207 lines (144 loc) · 9.71 KB
/
SPTextView.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
//
// SPTextView.m
// Simplenote
//
// Created by Tom Witkin on 7/19/13.
// Created by Michael Johnston on 7/19/13.
// Copyright (c) 2013 Automattic. All rights reserved.
//
#import "SPTextView.h"
#import <CoreFoundation/CFStringTokenizer.h>
#import "SPInteractiveTextStorage.h"
#import "Simplenote-Swift.h"
CGFloat const TextViewHighlightHorizontalPadding = 3;
CGFloat const TextViewHighlightCornerRadius = 3;
@implementation SPTextView
- (instancetype)init {
SPInteractiveTextStorage *textStorage = [[SPInteractiveTextStorage alloc] init];
NSTextContainer *container = [self setupTextContainerWith:textStorage];
self = [super initWithFrame:CGRectZero textContainer:container];
if (self) {
self.interactiveTextStorage = textStorage;
/*
Issue #188:
===========
On iOS 8, the text (was) getting clipped onscreen. Reason: the TextContainer was being shrunk down, and never re-expanded.
This was not happening on iOS 7, because [UITextView setScrollEnabled] method was disabling the
textContainer.heightTracksTextView property (and thus, the NSTextContainer instance was maintaining the CGFLOAT_MAX height.
As a workaround, we're disabling heightTracksTextView here, emulating iOS 7 behavior.
NOTE: Disabling heightTracksTextView before the init has a side effect. caretRectForPosition will not calculate the right
caret position.
*/
self.textContainer.heightTracksTextView = NO;
}
return self;
}
#pragma mark - Words
- (void)highlightRange:(NSRange)range animated:(BOOL)animated withBlock:(void (^)(CGRect))block {
[self clearHighlights:animated];
highlightViews = [NSMutableArray arrayWithCapacity:range.length];
[self.layoutManager enumerateLineFragmentsForGlyphRange:range
usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer *textContainer, NSRange glyphRange, BOOL *stop) {
NSInteger location = MAX(glyphRange.location, range.location);
NSInteger length = MIN(glyphRange.length, range.length - (location - range.location));
NSRange highlightRange = NSMakeRange(location, length);
CGRect highlightRect = [self.layoutManager boundingRectForGlyphRange:highlightRange
inTextContainer:textContainer];
highlightRect.origin.y += self.textContainerInset.top;
if (block)
block(highlightRect);
UIView *highlightView = [self createHighlightViewForAttributedString:[self.textStorage attributedSubstringFromRange:highlightRange]
frame:highlightRect];
[self addSubview:highlightView];
[self->highlightViews addObject:highlightView];
if (animated) {
[UIView animateWithDuration:0.1
animations:^{
highlightView.transform = CGAffineTransformMakeScale(1.2, 1.2);
} completion:^(BOOL finished) {
[UIView animateWithDuration:0.1
delay:0.0
usingSpringWithDamping:0.6
initialSpringVelocity:10.0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
highlightView.transform = CGAffineTransformIdentity;
} completion:nil];
}];
}
}];
}
- (UIView *)createHighlightViewForAttributedString:(NSAttributedString *)attributedString frame:(CGRect)frame {
frame.size.width += 2 * TextViewHighlightHorizontalPadding;
frame.origin.x -= TextViewHighlightHorizontalPadding;
UILabel *highlightLabel = [[UILabel alloc] initWithFrame:frame];
NSMutableAttributedString *mutableAttributedString = [[NSMutableAttributedString alloc] initWithAttributedString:attributedString];
[mutableAttributedString addAttributes:@{
NSForegroundColorAttributeName: [UIColor simplenoteEditorSearchHighlightTextColor],
NSBackgroundColorAttributeName: [UIColor simplenoteEditorSearchHighlightSelectedColor]
}
range:NSMakeRange(0, mutableAttributedString.length)];
highlightLabel.attributedText = mutableAttributedString;
highlightLabel.textAlignment = NSTextAlignmentCenter;
highlightLabel.backgroundColor = [UIColor simplenoteEditorSearchHighlightSelectedColor];
highlightLabel.layer.cornerRadius = TextViewHighlightCornerRadius;
highlightLabel.clipsToBounds = YES;
highlightLabel.layer.shouldRasterize = YES;
highlightLabel.layer.rasterizationScale = [[UIScreen mainScreen] scale];
return highlightLabel;
}
- (void)clearHighlights:(BOOL)animated {
if (animated) {
for (UIView *highlightView in highlightViews) {
UIView *highlightSnapshot = [highlightView snapshotViewAfterScreenUpdates:NO];
highlightSnapshot.frame = highlightView.frame;
[self addSubview:highlightSnapshot];
[UIView animateWithDuration:0.1
animations:^{
highlightSnapshot.alpha = 0.0;
highlightSnapshot.transform = CGAffineTransformMakeScale(0.0, 0.0);
} completion:^(BOOL finished) {
[highlightSnapshot removeFromSuperview];
}];
}
}
for (UIView *highlightView in highlightViews) {
[highlightView removeFromSuperview];
}
highlightViews = nil;
}
- (void)clearHighlights {
[self clearHighlights:NO];
}
#pragma mark -
- (void)scrollRectToVisible:(CGRect)rect animated:(BOOL)animated
{
// This is a work around for an issue where the text view scrolls to its bottom
// when a user selects all text. If the selected range matches the range of the
// string, this method was likely called as a result of choosing the "Select All" method
// of a UIMenuItem. In these cases we just return to avoid scrolling the view.
// For more info, see: https://github.com/Automattic/simplenote-ios/issues/263
NSRange range = [self.text rangeOfString:self.text];
NSRange selectedRange = self.selectedRange;
if (range.location == selectedRange.location &&
range.length == selectedRange.length) {
return;
}
[super scrollRectToVisible:rect animated:animated];
}
- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated
{
if (!self.enableScrollSmoothening || !animated) {
[super setContentOffset:contentOffset animated:animated];
return;
}
/// Secret Technique™
/// In order to _extremely_ match "Scroll to Selected Range" with any Keyboard animation, we'll introduce a custom animation.
/// This yields a smooth experience, whenever the keyboard is revealed (and the TextView decides to scroll along)
const UIViewAnimationOptions options = UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionLayoutSubviews;
const NSTimeInterval duration = 0.25;
[UIViewPropertyAnimator runningPropertyAnimatorWithDuration:duration delay:UIKitConstants.animationDelayZero options:options animations:^{
[self setContentOffset:contentOffset];
} completion:nil];
}
@end