forked from TextureGroup/Texture
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathASTextKitFontSizeAdjuster.mm
241 lines (191 loc) · 9.74 KB
/
ASTextKitFontSizeAdjuster.mm
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
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
//
// ASTextKitFontSizeAdjuster.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASTextKitFontSizeAdjuster.h>
#if AS_ENABLE_TEXTNODE
#import <tgmath.h>
#import <mutex>
#import <AsyncDisplayKit/ASLayoutManager.h>
#import <AsyncDisplayKit/ASTextKitContext.h>
#import <AsyncDisplayKit/ASThread.h>
//#define LOG(...) NSLog(__VA_ARGS__)
#define LOG(...)
@interface ASTextKitFontSizeAdjuster()
@property (nonatomic, readonly) NSLayoutManager *sizingLayoutManager;
@property (nonatomic, readonly) NSTextContainer *sizingTextContainer;
@end
@implementation ASTextKitFontSizeAdjuster
{
__weak ASTextKitContext *_context;
ASTextKitAttributes _attributes;
BOOL _measured;
CGFloat _scaleFactor;
AS::Mutex __instanceLock__;
}
@synthesize sizingLayoutManager = _sizingLayoutManager;
@synthesize sizingTextContainer = _sizingTextContainer;
- (instancetype)initWithContext:(ASTextKitContext *)context
constrainedSize:(CGSize)constrainedSize
textKitAttributes:(const ASTextKitAttributes &)textComponentAttributes;
{
if (self = [super init]) {
_context = context;
_constrainedSize = constrainedSize;
_attributes = textComponentAttributes;
}
return self;
}
+ (void)adjustFontSizeForAttributeString:(NSMutableAttributedString *)attrString withScaleFactor:(CGFloat)scaleFactor
{
if (scaleFactor == 1.0) return;
[attrString beginEditing];
// scale all the attributes that will change the bounding box
[attrString enumerateAttributesInRange:NSMakeRange(0, attrString.length) options:0 usingBlock:^(NSDictionary<NSString *,id> * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) {
if (attrs[NSFontAttributeName] != nil) {
UIFont *font = attrs[NSFontAttributeName];
font = [font fontWithSize:std::round(font.pointSize * scaleFactor)];
[attrString removeAttribute:NSFontAttributeName range:range];
[attrString addAttribute:NSFontAttributeName value:font range:range];
}
if (attrs[NSKernAttributeName] != nil) {
NSNumber *kerning = attrs[NSKernAttributeName];
[attrString removeAttribute:NSKernAttributeName range:range];
[attrString addAttribute:NSKernAttributeName value:@([kerning floatValue] * scaleFactor) range:range];
}
if (attrs[NSParagraphStyleAttributeName] != nil) {
NSMutableParagraphStyle *paragraphStyle = [attrs[NSParagraphStyleAttributeName] mutableCopy];
paragraphStyle.lineSpacing = (paragraphStyle.lineSpacing * scaleFactor);
paragraphStyle.paragraphSpacing = (paragraphStyle.paragraphSpacing * scaleFactor);
paragraphStyle.firstLineHeadIndent = (paragraphStyle.firstLineHeadIndent * scaleFactor);
paragraphStyle.headIndent = (paragraphStyle.headIndent * scaleFactor);
paragraphStyle.tailIndent = (paragraphStyle.tailIndent * scaleFactor);
paragraphStyle.minimumLineHeight = (paragraphStyle.minimumLineHeight * scaleFactor);
paragraphStyle.maximumLineHeight = (paragraphStyle.maximumLineHeight * scaleFactor);
[attrString removeAttribute:NSParagraphStyleAttributeName range:range];
[attrString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range];
}
}];
[attrString endEditing];
}
- (NSUInteger)lineCountForString:(NSAttributedString *)attributedString
{
NSUInteger lineCount = 0;
NSLayoutManager *sizingLayoutManager = [self sizingLayoutManager];
NSTextContainer *sizingTextContainer = [self sizingTextContainer];
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedString];
[textStorage addLayoutManager:sizingLayoutManager];
[sizingLayoutManager ensureLayoutForTextContainer:sizingTextContainer];
for (NSRange lineRange = { 0, 0 }; NSMaxRange(lineRange) < [sizingLayoutManager numberOfGlyphs] && lineCount <= _attributes.maximumNumberOfLines; lineCount++) {
[sizingLayoutManager lineFragmentRectForGlyphAtIndex:NSMaxRange(lineRange) effectiveRange:&lineRange];
}
[textStorage removeLayoutManager:sizingLayoutManager];
return lineCount;
}
- (CGSize)boundingBoxForString:(NSAttributedString *)attributedString
{
NSLayoutManager *sizingLayoutManager = [self sizingLayoutManager];
NSTextContainer *sizingTextContainer = [self sizingTextContainer];
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedString];
[textStorage addLayoutManager:sizingLayoutManager];
[sizingLayoutManager ensureLayoutForTextContainer:sizingTextContainer];
CGRect textRect = [sizingLayoutManager boundingRectForGlyphRange:NSMakeRange(0, [textStorage length])
inTextContainer:sizingTextContainer];
[textStorage removeLayoutManager:sizingLayoutManager];
return textRect.size;
}
- (NSLayoutManager *)sizingLayoutManager
{
AS::MutexLocker l(__instanceLock__);
if (_sizingLayoutManager == nil) {
_sizingLayoutManager = [[ASLayoutManager alloc] init];
_sizingLayoutManager.usesFontLeading = NO;
if (_sizingTextContainer == nil) {
// make this text container unbounded in height so that the layout manager will compute the total
// number of lines and not stop counting when height runs out.
_sizingTextContainer = [[NSTextContainer alloc] initWithSize:CGSizeMake(_constrainedSize.width, CGFLOAT_MAX)];
_sizingTextContainer.lineFragmentPadding = 0;
// use 0 regardless of what is in the attributes so that we get an accurate line count
_sizingTextContainer.maximumNumberOfLines = 0;
_sizingTextContainer.lineBreakMode = _attributes.lineBreakMode;
_sizingTextContainer.exclusionPaths = _attributes.exclusionPaths;
}
[_sizingLayoutManager addTextContainer:_sizingTextContainer];
}
return _sizingLayoutManager;
}
- (CGFloat)scaleFactor
{
if (_measured) {
return _scaleFactor;
}
if ([_attributes.pointSizeScaleFactors count] == 0 || isinf(_constrainedSize.width)) {
_measured = YES;
_scaleFactor = 1.0;
return _scaleFactor;
}
__block CGFloat adjustedScale = 1.0;
// We add the scale factor of 1 to our scaleFactors array so that in the first iteration of the loop below, we are
// actually determining if we need to scale at all. If something doesn't fit, we will continue to iterate our scale factors.
NSArray *scaleFactors = [@[@(1)] arrayByAddingObjectsFromArray:_attributes.pointSizeScaleFactors];
[_context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) {
// Check for two different situations (and correct for both)
// 1. The longest word in the string fits without being wrapped
// 2. The entire text fits in the given constrained size.
NSString *str = textStorage.string;
NSArray *words = [str componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
NSString *longestWordNeedingResize = @"";
for (NSString *word in words) {
if ([word length] > [longestWordNeedingResize length]) {
longestWordNeedingResize = word;
}
}
// check to see if we may need to shrink for any of these things
BOOL longestWordFits = [longestWordNeedingResize length] ? NO : YES;
BOOL maxLinesFits = self->_attributes.maximumNumberOfLines > 0 ? NO : YES;
BOOL heightFits = isinf(self->_constrainedSize.height) ? YES : NO;
CGSize longestWordSize = CGSizeZero;
if (longestWordFits == NO) {
NSRange longestWordRange = [str rangeOfString:longestWordNeedingResize];
NSAttributedString *attrString = [textStorage attributedSubstringFromRange:longestWordRange];
longestWordSize = [attrString boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin context:nil].size;
}
// we may need to shrink for some reason, so let's iterate through our scale factors to see if we actually need to shrink
// Note: the first scale factor in the array is 1.0 so will make sure that things don't fit without shrinking
for (NSNumber *adjustedScaleObj in scaleFactors) {
if (longestWordFits && maxLinesFits && heightFits) {
break;
}
adjustedScale = [adjustedScaleObj floatValue];
if (longestWordFits == NO) {
// we need to check the longest word to make sure it fits
longestWordFits = std::ceil((longestWordSize.width * adjustedScale) <= self->_constrainedSize.width);
}
// if the longest word fits, go ahead and check max line and height. If it didn't fit continue to the next scale factor
if (longestWordFits == YES) {
// scale our string by the current scale factor
NSMutableAttributedString *scaledString = [[NSMutableAttributedString alloc] initWithAttributedString:textStorage];
[[self class] adjustFontSizeForAttributeString:scaledString withScaleFactor:adjustedScale];
// check to see if this scaled string fit in the max lines
if (maxLinesFits == NO) {
maxLinesFits = ([self lineCountForString:scaledString] <= self->_attributes.maximumNumberOfLines);
}
// if max lines still doesn't fit, continue without checking that we fit in the constrained height
if (maxLinesFits == YES && heightFits == NO) {
// max lines fit so make sure that we fit in the constrained height.
CGSize stringSize = [self boundingBoxForString:scaledString];
heightFits = (stringSize.height <= self->_constrainedSize.height);
}
}
}
}];
_measured = YES;
_scaleFactor = adjustedScale;
return _scaleFactor;
}
@end
#endif