mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-24 07:05:35 +00:00
Merge commit '4355f4d2eef9472642a8a8029aeef1c7fdad034f' into debug-drawrect
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
Pod::Spec.new do |spec|
|
||||
spec.name = 'AsyncDisplayKit'
|
||||
spec.version = '2.0-rc.1'
|
||||
spec.version = '2.0-rc.2'
|
||||
spec.license = { :type => 'BSD' }
|
||||
spec.homepage = 'http://asyncdisplaykit.org'
|
||||
spec.authors = { 'Scott Goodson' => 'scottgoodson@gmail.com' }
|
||||
|
||||
@@ -123,8 +123,8 @@ static NSMutableSet *__cellClassesForVisibilityNotifications = nil; // See +init
|
||||
- (void)_locked_displayNodeDidInvalidateSizeNewSize:(CGSize)newSize
|
||||
{
|
||||
CGSize oldSize = self.bounds.size;
|
||||
[super _locked_displayNodeDidInvalidateSizeNewSize:newSize];
|
||||
if (CGSizeEqualToSize(oldSize, newSize) == NO) {
|
||||
self.frame = {self.frame.origin, newSize};
|
||||
[self didRelayoutFromOldSize:oldSize toNewSize:newSize];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -644,7 +644,9 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (indexPath.item >= [self numberOfItemsInSection:section]) {
|
||||
NSInteger item = indexPath.item;
|
||||
// item == NSNotFound means e.g. "scroll to this section" and is acceptable
|
||||
if (item != NSNotFound && item >= [self numberOfItemsInSection:section]) {
|
||||
ASDisplayNodeFailAssert(@"Collection view index path has invalid item %lu in section %lu, item count = %lu", (unsigned long)indexPath.item, (unsigned long)section, (unsigned long)[self numberOfItemsInSection:section]);
|
||||
return nil;
|
||||
}
|
||||
|
||||
@@ -18,11 +18,11 @@
|
||||
#import "ASAvailability.h"
|
||||
#import "ASDisplayNode+Subclasses.h"
|
||||
#import "ASDisplayNode+FrameworkPrivate.h"
|
||||
#import "ASDisplayNodeExtras.h"
|
||||
#import "ASLog.h"
|
||||
#import "ASPhotosFrameworkImageRequest.h"
|
||||
#import "ASEqualityHelpers.h"
|
||||
#import "ASInternalHelpers.h"
|
||||
#import "ASDisplayNodeExtras.h"
|
||||
|
||||
#if !AS_IOS8_SDK_OR_LATER
|
||||
#error ASMultiplexImageNode can be used on iOS 7, but must be linked against the iOS 8 SDK.
|
||||
@@ -235,7 +235,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent
|
||||
|
||||
// setting this to nil makes the node fetch images the next time its display starts
|
||||
_loadedImageIdentifier = nil;
|
||||
self.image = nil;
|
||||
[self _setImage:nil];
|
||||
}
|
||||
|
||||
- (void)didEnterPreloadState
|
||||
@@ -327,6 +327,17 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent
|
||||
|
||||
#pragma mark - Core
|
||||
|
||||
- (void)setImage:(UIImage *)image
|
||||
{
|
||||
ASDisplayNodeAssert(NO, @"Setting the image directly on an ASMultiplexImageNode is unsafe. It will be cleared in didExitPreloadRange and will have no way to restore in didEnterPreloadRange");
|
||||
super.image = image;
|
||||
}
|
||||
|
||||
- (void)_setImage:(UIImage *)image
|
||||
{
|
||||
super.image = image;
|
||||
}
|
||||
|
||||
- (void)setDelegate:(id <ASMultiplexImageNodeDelegate>)delegate
|
||||
{
|
||||
if (_delegate == delegate)
|
||||
@@ -522,7 +533,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent
|
||||
if (ASObjectIsEqual(strongSelf->_downloadIdentifier, downloadIdentifier) == NO && downloadIdentifier != nil) {
|
||||
return;
|
||||
}
|
||||
strongSelf.image = progressImage;
|
||||
[strongSelf _setImage:progressImage];
|
||||
};
|
||||
}
|
||||
[_downloader setProgressImageBlock:progress callbackQueue:dispatch_get_main_queue() withDownloadIdentifier:_downloadIdentifier];
|
||||
@@ -540,7 +551,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent
|
||||
if (shouldReleaseImageOnBackgroundThread) {
|
||||
ASPerformBackgroundDeallocation(image);
|
||||
}
|
||||
self.image = nil;
|
||||
[self _setImage:nil];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
@@ -869,7 +880,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent
|
||||
UIImage *previousImage = self.image;
|
||||
|
||||
self.loadedImageIdentifier = imageIdentifier;
|
||||
self.image = image;
|
||||
[self _setImage:image];
|
||||
|
||||
if (_delegateFlags.updatedImage) {
|
||||
[_delegate multiplexImageNode:self didUpdateImage:image withIdentifier:imageIdentifier fromImage:previousImage withIdentifier:previousIdentifier];
|
||||
|
||||
@@ -28,7 +28,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@interface ASNetworkImageNode : ASImageNode
|
||||
|
||||
/**
|
||||
* The designated initializer. Cache and Downloader are WEAK references.
|
||||
* The designated initializer. Cache and Downloader are WEAK references.
|
||||
*
|
||||
* @param cache The object that implements a cache of images for the image node. Weak reference.
|
||||
* @param downloader The object that implements image downloading for the image node. Must not be nil. Weak reference.
|
||||
@@ -40,7 +40,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
- (instancetype)initWithCache:(nullable id<ASImageCacheProtocol>)cache downloader:(id<ASImageDownloaderProtocol>)downloader NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
/**
|
||||
* Convenience initialiser.
|
||||
* Convenience initializer.
|
||||
*
|
||||
* @return An ASNetworkImageNode configured to use the NSURLSession-powered ASBasicImageDownloader, and no extra cache.
|
||||
*/
|
||||
@@ -51,6 +51,17 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
*/
|
||||
@property (nullable, nonatomic, weak, readwrite) id<ASNetworkImageNodeDelegate> delegate;
|
||||
|
||||
/**
|
||||
* The image to display.
|
||||
*
|
||||
* @discussion By setting an image to the image property the ASNetworkImageNode will act like a plain ASImageNode.
|
||||
* As soon as the URL is set the ASNetworkImageNode will act like an ASNetworkImageNode and the image property
|
||||
* will be managed internally. This means the image property will be cleared out and replaced by the placeholder
|
||||
* (<defaultImage>) image while loading and the final image after the new image data was downloaded and processed.
|
||||
* If you want to use a placholder image functionality use the defaultImage property instead.
|
||||
*/
|
||||
@property (nullable, nonatomic, strong) UIImage *image;
|
||||
|
||||
/**
|
||||
* A placeholder image to display while the URL is loading.
|
||||
*/
|
||||
@@ -59,7 +70,9 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
/**
|
||||
* The URL of a new image to download and display.
|
||||
*
|
||||
* @discussion Changing this property will reset the displayed image to a placeholder (<defaultImage>) while loading.
|
||||
* @discussion By setting an URL, the image property of this node will be managed internally. This means previously
|
||||
* directly set images to the image property will be cleared out and replaced by the placeholder (<defaultImage>) image
|
||||
* while loading and the final image after the new image data was downloaded and processed.
|
||||
*/
|
||||
@property (nullable, nonatomic, strong, readwrite) NSURL *URL;
|
||||
|
||||
@@ -67,8 +80,11 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
* Download and display a new image.
|
||||
*
|
||||
* @param URL The URL of a new image to download and display.
|
||||
*
|
||||
* @param reset Whether to display a placeholder (<defaultImage>) while loading the new image.
|
||||
*
|
||||
* @discussion By setting an URL, the image property of this node will be managed internally. This means previously
|
||||
* directly set images to the image property will be cleared out and replaced by the placeholder (<defaultImage>) image
|
||||
* while loading and the final image after the new image data was downloaded and processed.
|
||||
*/
|
||||
- (void)setURL:(nullable NSURL *)URL resetToDefault:(BOOL)reset;
|
||||
|
||||
|
||||
@@ -14,12 +14,12 @@
|
||||
|
||||
#import "ASBasicImageDownloader.h"
|
||||
#import "ASDisplayNodeInternal.h"
|
||||
#import "ASDisplayNodeExtras.h"
|
||||
#import "ASDisplayNode+Subclasses.h"
|
||||
#import "ASDisplayNode+FrameworkPrivate.h"
|
||||
#import "ASEqualityHelpers.h"
|
||||
#import "ASInternalHelpers.h"
|
||||
#import "ASImageContainerProtocolCategories.h"
|
||||
#import "ASDisplayNodeExtras.h"
|
||||
|
||||
#if PIN_REMOTE_IMAGE
|
||||
#import "ASPINRemoteImageDownloader.h"
|
||||
@@ -44,6 +44,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0};
|
||||
id _downloadIdentifierForProgressBlock;
|
||||
|
||||
BOOL _imageLoaded;
|
||||
BOOL _imageWasSetExternally;
|
||||
CGFloat _currentImageQuality;
|
||||
CGFloat _renderedImageQuality;
|
||||
BOOL _shouldRenderProgressImages;
|
||||
@@ -69,10 +70,13 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0};
|
||||
unsigned int cacheSupportsSynchronousFetch:1;
|
||||
} _cacheFlags;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation ASNetworkImageNode
|
||||
|
||||
@dynamic image;
|
||||
|
||||
- (instancetype)initWithCache:(id<ASImageCacheProtocol>)cache downloader:(id<ASImageDownloaderProtocol>)downloader
|
||||
{
|
||||
if (!(self = [super init]))
|
||||
@@ -118,6 +122,25 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0};
|
||||
|
||||
#pragma mark - Public methods -- must lock
|
||||
|
||||
/// Setter for public image property. It has the side effect to set an internal _imageWasSetExternally that prevents setting an image internally. Setting an image internally should happen with the _setImage: method
|
||||
- (void)setImage:(UIImage *)image
|
||||
{
|
||||
ASDN::MutexLocker l(__instanceLock__);
|
||||
|
||||
_imageWasSetExternally = (image != nil);
|
||||
if (_imageWasSetExternally) {
|
||||
[self _cancelDownloadAndClearImage];
|
||||
_URL = nil;
|
||||
}
|
||||
|
||||
[self _setImage:image];
|
||||
}
|
||||
|
||||
- (void)_setImage:(UIImage *)image
|
||||
{
|
||||
super.image = image;
|
||||
}
|
||||
|
||||
- (void)setURL:(NSURL *)URL
|
||||
{
|
||||
[self setURL:URL resetToDefault:YES];
|
||||
@@ -127,6 +150,8 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0};
|
||||
{
|
||||
ASDN::MutexLocker l(__instanceLock__);
|
||||
|
||||
_imageWasSetExternally = NO;
|
||||
|
||||
if (ASObjectIsEqual(URL, _URL)) {
|
||||
return;
|
||||
}
|
||||
@@ -138,7 +163,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0};
|
||||
|
||||
BOOL hasURL = _URL == nil;
|
||||
if (reset || hasURL) {
|
||||
self.image = _defaultImage;
|
||||
[self _setImage:_defaultImage];
|
||||
/* We want to maintain the order that currentImageQuality is set regardless of the calling thread,
|
||||
so always use a dispatch_async to ensure that we queue the operations in the correct order.
|
||||
(see comment in displayDidFinish) */
|
||||
@@ -173,7 +198,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0};
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
self.currentImageQuality = hasURL ? 0.0 : 1.0;
|
||||
});
|
||||
self.image = defaultImage;
|
||||
[self _setImage:defaultImage];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,7 +283,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0};
|
||||
if (_imageLoaded == NO && _URL && _downloadIdentifier == nil) {
|
||||
UIImage *result = [[_cache synchronouslyFetchedCachedImageWithURL:_URL] asdk_image];
|
||||
if (result) {
|
||||
self.image = result;
|
||||
[self _setImage:result];
|
||||
_imageLoaded = YES;
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
_currentImageQuality = 1.0;
|
||||
@@ -314,12 +339,12 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0};
|
||||
|
||||
{
|
||||
ASDN::MutexLocker l(__instanceLock__);
|
||||
|
||||
[self _cancelImageDownload];
|
||||
[self _clearImage];
|
||||
if (_cacheFlags.cacheSupportsClearing) {
|
||||
[_cache clearFetchedImageFromCacheWithURL:_URL];
|
||||
// If the image was set explicitly we don't want to remove it while exiting the preload state
|
||||
if (_imageWasSetExternally) {
|
||||
return;
|
||||
}
|
||||
|
||||
[self _cancelDownloadAndClearImage];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,6 +354,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0};
|
||||
|
||||
{
|
||||
ASDN::MutexLocker l(__instanceLock__);
|
||||
// Image was set externally no need to load an image
|
||||
[self _lazilyLoadImageIfNecessary];
|
||||
}
|
||||
}
|
||||
@@ -342,7 +368,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0};
|
||||
if (ASObjectIsEqual(_downloadIdentifier, downloadIdentifier) == NO && downloadIdentifier != nil) {
|
||||
return;
|
||||
}
|
||||
self.image = progressImage;
|
||||
[self _setImage:progressImage];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
// See comment in -displayDidFinish for why this must be dispatched to main
|
||||
self.currentImageQuality = progress;
|
||||
@@ -385,25 +411,13 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0};
|
||||
_downloadIdentifierForProgressBlock = newDownloadIDForProgressBlock;
|
||||
}
|
||||
|
||||
- (void)_clearImage
|
||||
- (void)_cancelDownloadAndClearImage
|
||||
{
|
||||
// Destruction of bigger images on the main thread can be expensive
|
||||
// and can take some time, so we dispatch onto a bg queue to
|
||||
// actually dealloc.
|
||||
UIImage *image = self.image;
|
||||
CGSize imageSize = image.size;
|
||||
BOOL shouldReleaseImageOnBackgroundThread = imageSize.width > kMinReleaseImageOnBackgroundSize.width ||
|
||||
imageSize.height > kMinReleaseImageOnBackgroundSize.height;
|
||||
if (shouldReleaseImageOnBackgroundThread) {
|
||||
ASPerformBackgroundDeallocation(image);
|
||||
[self _cancelImageDownload];
|
||||
[self _clearImage];
|
||||
if (_cacheFlags.cacheSupportsClearing) {
|
||||
[_cache clearFetchedImageFromCacheWithURL:_URL];
|
||||
}
|
||||
self.animatedImage = nil;
|
||||
self.image = _defaultImage;
|
||||
_imageLoaded = NO;
|
||||
// See comment in -displayDidFinish for why this must be dispatched to main
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
self.currentImageQuality = 0.0;
|
||||
});
|
||||
}
|
||||
|
||||
- (void)_cancelImageDownload
|
||||
@@ -420,6 +434,27 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0};
|
||||
_cacheUUID = nil;
|
||||
}
|
||||
|
||||
- (void)_clearImage
|
||||
{
|
||||
// Destruction of bigger images on the main thread can be expensive
|
||||
// and can take some time, so we dispatch onto a bg queue to
|
||||
// actually dealloc.
|
||||
UIImage *image = self.image;
|
||||
CGSize imageSize = image.size;
|
||||
BOOL shouldReleaseImageOnBackgroundThread = imageSize.width > kMinReleaseImageOnBackgroundSize.width ||
|
||||
imageSize.height > kMinReleaseImageOnBackgroundSize.height;
|
||||
if (shouldReleaseImageOnBackgroundThread) {
|
||||
ASPerformBackgroundDeallocation(image);
|
||||
}
|
||||
self.animatedImage = nil;
|
||||
[self _setImage:_defaultImage];
|
||||
_imageLoaded = NO;
|
||||
// See comment in -displayDidFinish for why this must be dispatched to main
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
self.currentImageQuality = 0.0;
|
||||
});
|
||||
}
|
||||
|
||||
- (void)_downloadImageWithCompletion:(void (^)(id <ASImageContainerProtocol> imageContainer, NSError*, id downloadIdentifier))finished
|
||||
{
|
||||
ASPerformBlockOnBackgroundThread(^{
|
||||
@@ -458,7 +493,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0};
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (self.shouldCacheImage) {
|
||||
self.image = [UIImage imageNamed:_URL.path.lastPathComponent];
|
||||
[self _setImage:[UIImage imageNamed:_URL.path.lastPathComponent]];
|
||||
} else {
|
||||
// First try to load the path directly, for efficiency assuming a developer who
|
||||
// doesn't want caching is trying to be as minimal as possible.
|
||||
@@ -488,7 +523,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0};
|
||||
if (animatedImage != nil) {
|
||||
self.animatedImage = animatedImage;
|
||||
} else {
|
||||
self.image = nonAnimatedImage;
|
||||
[self _setImage:nonAnimatedImage];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -524,7 +559,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0};
|
||||
if ([imageContainer asdk_animatedImageData] && _downloaderFlags.downloaderImplementsAnimatedImage) {
|
||||
strongSelf.animatedImage = [_downloader animatedImageWithData:[imageContainer asdk_animatedImageData]];
|
||||
} else {
|
||||
strongSelf.image = [imageContainer asdk_image];
|
||||
[strongSelf _setImage:[imageContainer asdk_image]];
|
||||
}
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
strongSelf->_currentImageQuality = 1.0;
|
||||
|
||||
@@ -581,7 +581,9 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (indexPath.item >= [self numberOfRowsInSection:section]) {
|
||||
NSInteger item = indexPath.item;
|
||||
// item == NSNotFound means e.g. "scroll to this section" and is acceptable
|
||||
if (item != NSNotFound && item >= [self numberOfRowsInSection:section]) {
|
||||
ASDisplayNodeFailAssert(@"Table view index path has invalid item %lu in section %lu, item count = %lu", (unsigned long)indexPath.item, (unsigned long)section, (unsigned long)[self numberOfRowsInSection:section]);
|
||||
return nil;
|
||||
}
|
||||
|
||||
@@ -50,6 +50,76 @@ struct ASTextNodeDrawParameter {
|
||||
UIColor *backgroundColor;
|
||||
};
|
||||
|
||||
#pragma mark - ASTextKitRenderer
|
||||
|
||||
// Not used at the moment but handy to have
|
||||
/*ASDISPLAYNODE_INLINE NSUInteger ASHashFromCGRect(CGRect rect)
|
||||
{
|
||||
return ((*(NSUInteger *)&rect.origin.x << 10 ^ *(NSUInteger *)&rect.origin.y) + (*(NSUInteger *)&rect.size.width << 10 ^ *(NSUInteger *)&rect.size.height));
|
||||
}*/
|
||||
|
||||
ASDISPLAYNODE_INLINE NSUInteger ASHashFromCGSize(CGSize size)
|
||||
{
|
||||
return ((*(NSUInteger *)&size.width << 10 ^ *(NSUInteger *)&size.height));
|
||||
}
|
||||
|
||||
@interface ASTextNodeRendererKey : NSObject
|
||||
@property (assign, nonatomic) ASTextKitAttributes attributes;
|
||||
@property (assign, nonatomic) CGSize constrainedSize;
|
||||
@end
|
||||
|
||||
@implementation ASTextNodeRendererKey
|
||||
|
||||
- (NSUInteger)hash
|
||||
{
|
||||
return _attributes.hash() ^ ASHashFromCGSize(_constrainedSize);
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(ASTextNodeRendererKey *)object
|
||||
{
|
||||
if (self == object) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
return _attributes == object.attributes && CGSizeEqualToSize(_constrainedSize, object.constrainedSize);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
static NSCache *sharedRendererCache()
|
||||
{
|
||||
static dispatch_once_t onceToken;
|
||||
static NSCache *__rendererCache = nil;
|
||||
dispatch_once(&onceToken, ^{
|
||||
__rendererCache = [[NSCache alloc] init];
|
||||
__rendererCache.countLimit = 500; // 500 renders cache
|
||||
});
|
||||
return __rendererCache;
|
||||
}
|
||||
|
||||
/**
|
||||
The concept here is that neither the node nor layout should ever have a strong reference to the renderer object.
|
||||
This is to reduce memory load when loading thousands and thousands of text nodes into memory at once. Instead
|
||||
we maintain a LRU renderer cache that is queried via a unique key based on text kit attributes and constrained size.
|
||||
*/
|
||||
|
||||
static ASTextKitRenderer *rendererForAttributes(ASTextKitAttributes attributes, CGSize constrainedSize)
|
||||
{
|
||||
NSCache *cache = sharedRendererCache();
|
||||
|
||||
ASTextNodeRendererKey *key = [[ASTextNodeRendererKey alloc] init];
|
||||
key.attributes = attributes;
|
||||
key.constrainedSize = constrainedSize;
|
||||
|
||||
ASTextKitRenderer *renderer = [cache objectForKey:key];
|
||||
if (renderer == nil) {
|
||||
renderer = [[ASTextKitRenderer alloc] initWithTextKitAttributes:attributes constrainedSize:constrainedSize];
|
||||
[cache setObject:renderer forKey:key];
|
||||
}
|
||||
|
||||
return renderer;
|
||||
}
|
||||
|
||||
@interface ASTextNode () <UIGestureRecognizerDelegate>
|
||||
|
||||
@end
|
||||
@@ -73,10 +143,6 @@ struct ASTextNodeDrawParameter {
|
||||
NSRange _highlightRange;
|
||||
ASHighlightOverlayLayer *_activeHighlightLayer;
|
||||
|
||||
CGSize _constrainedSize;
|
||||
|
||||
ASTextKitRenderer *_renderer;
|
||||
|
||||
ASTextNodeDrawParameter _drawParameter;
|
||||
|
||||
UILongPressGestureRecognizer *_longPressGestureRecognizer;
|
||||
@@ -123,8 +189,6 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
|
||||
self.isAccessibilityElement = YES;
|
||||
self.accessibilityTraits = UIAccessibilityTraitStaticText;
|
||||
|
||||
_constrainedSize = CGSizeMake(-INFINITY, -INFINITY);
|
||||
|
||||
// Placeholders
|
||||
// Disabled by default in ASDisplayNode, but add a few options for those who toggle
|
||||
// on the special placeholder behavior of ASTextNode.
|
||||
@@ -139,8 +203,6 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
|
||||
{
|
||||
CGColorRelease(_shadowColor);
|
||||
|
||||
[self _invalidateRenderer];
|
||||
|
||||
if (_longPressGestureRecognizer) {
|
||||
_longPressGestureRecognizer.delegate = nil;
|
||||
[_longPressGestureRecognizer removeTarget:nil action:NULL];
|
||||
@@ -181,27 +243,12 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
|
||||
|
||||
#pragma mark - ASDisplayNode
|
||||
|
||||
// FIXME: Re-evaluate if it is still the right decision to clear the renderer at this stage.
|
||||
// This code was written before TextKit and when 512MB devices were still the overwhelming majority.
|
||||
- (void)displayDidFinish
|
||||
{
|
||||
[super displayDidFinish];
|
||||
|
||||
// We invalidate our renderer here to clear the very high memory cost of
|
||||
// keeping this around. _invalidateRenderer will dealloc this onto a bg
|
||||
// thread resulting in less stutters on the main thread than if it were
|
||||
// to be deallocated in dealloc. This is also helpful in opportunistically
|
||||
// reducing memory consumption and reducing the overall footprint of the app.
|
||||
[self _invalidateRenderer];
|
||||
}
|
||||
|
||||
- (void)clearContents
|
||||
{
|
||||
// We discard the backing store and renderer to prevent the very large
|
||||
// memory overhead of maintaining these for all text nodes. They can be
|
||||
// regenerated when layout is necessary.
|
||||
[super clearContents]; // ASDisplayNode will set layer.contents = nil
|
||||
[self _invalidateRenderer];
|
||||
}
|
||||
|
||||
- (void)didLoad
|
||||
@@ -218,45 +265,23 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setFrame:(CGRect)frame
|
||||
{
|
||||
[super setFrame:frame];
|
||||
[self _invalidateRendererIfNeededForBoundsSize:frame.size];
|
||||
}
|
||||
|
||||
- (void)setBounds:(CGRect)bounds
|
||||
{
|
||||
[super setBounds:bounds];
|
||||
[self _invalidateRendererIfNeededForBoundsSize:bounds.size];
|
||||
}
|
||||
|
||||
#pragma mark - Renderer Management
|
||||
|
||||
- (ASTextKitRenderer *)_renderer
|
||||
{
|
||||
return [self _rendererWithBounds:self.threadSafeBounds];
|
||||
CGSize constrainedSize = self.threadSafeBounds.size;
|
||||
return [self _rendererWithBoundsSlow:{.size = constrainedSize}];
|
||||
}
|
||||
|
||||
- (ASTextKitRenderer *)_rendererWithBounds:(CGRect)bounds
|
||||
- (ASTextKitRenderer *)_rendererWithBoundsSlow:(CGRect)bounds
|
||||
{
|
||||
ASDN::MutexLocker l(__instanceLock__);
|
||||
|
||||
if (_renderer == nil) {
|
||||
CGSize constrainedSize;
|
||||
if (_constrainedSize.width != -INFINITY) {
|
||||
constrainedSize = _constrainedSize;
|
||||
} else {
|
||||
constrainedSize = bounds.size;
|
||||
constrainedSize.width -= (_textContainerInset.left + _textContainerInset.right);
|
||||
constrainedSize.height -= (_textContainerInset.top + _textContainerInset.bottom);
|
||||
}
|
||||
|
||||
_renderer = [[ASTextKitRenderer alloc] initWithTextKitAttributes:[self _rendererAttributes]
|
||||
constrainedSize:constrainedSize];
|
||||
}
|
||||
return _renderer;
|
||||
bounds.size.width -= (_textContainerInset.left + _textContainerInset.right);
|
||||
bounds.size.height -= (_textContainerInset.top + _textContainerInset.bottom);
|
||||
return rendererForAttributes([self _rendererAttributes], bounds.size);
|
||||
}
|
||||
|
||||
|
||||
- (ASTextKitAttributes)_rendererAttributes
|
||||
{
|
||||
ASDN::MutexLocker l(__instanceLock__);
|
||||
@@ -276,38 +301,6 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
|
||||
};
|
||||
}
|
||||
|
||||
- (void)_invalidateRendererIfNeeded
|
||||
{
|
||||
[self _invalidateRendererIfNeededForBoundsSize:self.threadSafeBounds.size];
|
||||
}
|
||||
|
||||
- (void)_invalidateRendererIfNeededForBoundsSize:(CGSize)boundsSize
|
||||
{
|
||||
if ([self _needInvalidateRendererForBoundsSize:boundsSize]) {
|
||||
// Our bounds have changed to a size that is not identical to our constraining size,
|
||||
// so our previous layout information is invalid, and TextKit may draw at the
|
||||
// incorrect origin.
|
||||
{
|
||||
ASDN::MutexLocker l(__instanceLock__);
|
||||
_constrainedSize = CGSizeMake(-INFINITY, -INFINITY);
|
||||
}
|
||||
[self _invalidateRenderer];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_invalidateRenderer
|
||||
{
|
||||
ASDN::MutexLocker l(__instanceLock__);
|
||||
|
||||
if (_renderer) {
|
||||
// Destruction of the layout managers/containers/text storage is quite
|
||||
// expensive, and can take some time, so we dispatch onto a bg queue to
|
||||
// actually dealloc.
|
||||
ASPerformBackgroundDeallocation(_renderer);
|
||||
_renderer = nil;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Layout and Sizing
|
||||
|
||||
- (void)setTextContainerInset:(UIEdgeInsets)textContainerInset
|
||||
@@ -327,60 +320,6 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
|
||||
return _textContainerInset;
|
||||
}
|
||||
|
||||
- (BOOL)_needInvalidateRendererForBoundsSize:(CGSize)boundsSize
|
||||
{
|
||||
ASDN::MutexLocker l(__instanceLock__);
|
||||
|
||||
if (_renderer == nil) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
// If the size is not the same as the constraint we provided to the renderer, start out assuming we need
|
||||
// a new one. However, there are common cases where the constrained size doesn't need to be the same as calculated.
|
||||
CGSize rendererConstrainedSize = _renderer.constrainedSize;
|
||||
|
||||
//inset bounds
|
||||
boundsSize.width -= _textContainerInset.left + _textContainerInset.right;
|
||||
boundsSize.height -= _textContainerInset.top + _textContainerInset.bottom;
|
||||
|
||||
if (CGSizeEqualToSize(boundsSize, rendererConstrainedSize)) {
|
||||
return NO;
|
||||
} else {
|
||||
// It is very common to have a constrainedSize with a concrete, specific width but +Inf height.
|
||||
// In this case, as long as the text node has bounds as large as the full calculatedLayout suggests,
|
||||
// it means that the text has all the room it needs (as it was not vertically bounded). So, we will not
|
||||
// experience truncation and don't need to recreate the renderer with the size it already calculated,
|
||||
// as this would essentially serve to set its constrainedSize to be its calculatedSize (unnecessary).
|
||||
ASLayout *layout = self.calculatedLayout;
|
||||
if (layout != nil && CGSizeEqualToSize(boundsSize, layout.size)) {
|
||||
return (boundsSize.width != rendererConstrainedSize.width);
|
||||
} else {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)calculatedLayoutDidChange
|
||||
{
|
||||
[super calculatedLayoutDidChange];
|
||||
|
||||
ASLayout *layout = self.calculatedLayout;
|
||||
|
||||
if (layout != nil) {
|
||||
ASDN::MutexLocker l(__instanceLock__);
|
||||
CGSize layoutSize = layout.size;
|
||||
|
||||
// Apply textContainerInset
|
||||
layoutSize.width -= (_textContainerInset.left + _textContainerInset.right);
|
||||
layoutSize.height -= (_textContainerInset.top + _textContainerInset.bottom);
|
||||
|
||||
if (CGSizeEqualToSize(_constrainedSize, layoutSize) == NO) {
|
||||
_constrainedSize = layoutSize;
|
||||
[self _invalidateRenderer];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize
|
||||
{
|
||||
ASDN::MutexLocker l(__instanceLock__);
|
||||
@@ -390,27 +329,18 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
|
||||
|
||||
// Cache the original constrained size for final size calculateion
|
||||
CGSize originalConstrainedSize = constrainedSize;
|
||||
|
||||
// Adjust constrainedSize for textContainerInset before assigning it
|
||||
constrainedSize.width -= (_textContainerInset.left + _textContainerInset.right);
|
||||
constrainedSize.height -= (_textContainerInset.top + _textContainerInset.bottom);
|
||||
|
||||
_constrainedSize = constrainedSize;
|
||||
|
||||
if (_renderer != nil && CGSizeEqualToSize(constrainedSize, _renderer.constrainedSize) == NO) {
|
||||
[self _invalidateRenderer];
|
||||
}
|
||||
|
||||
[self setNeedsDisplay];
|
||||
|
||||
CGSize size = [self _renderer].size;
|
||||
ASTextKitRenderer *renderer = [self _rendererWithBoundsSlow:{.size = constrainedSize}];
|
||||
CGSize size = renderer.size;
|
||||
if (_attributedText.length > 0) {
|
||||
self.style.ascender = [[self class] ascenderWithAttributedString:_attributedText];
|
||||
self.style.descender = [[_attributedText attribute:NSFontAttributeName atIndex:_attributedText.length - 1 effectiveRange:NULL] descender];
|
||||
if (_renderer.currentScaleFactor > 0 && _renderer.currentScaleFactor < 1.0) {
|
||||
if (renderer.currentScaleFactor > 0 && renderer.currentScaleFactor < 1.0) {
|
||||
// while not perfect, this is a good estimate of what the ascender of the scaled font will be.
|
||||
self.style.ascender *= _renderer.currentScaleFactor;
|
||||
self.style.descender *= _renderer.currentScaleFactor;
|
||||
self.style.ascender *= renderer.currentScaleFactor;
|
||||
self.style.descender *= renderer.currentScaleFactor;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -461,9 +391,6 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
|
||||
// Without this, the size calculation of the text with truncation applied will
|
||||
// not take into account the attributes of attributedText in the last line
|
||||
[self _updateComposedTruncationText];
|
||||
|
||||
// We need an entirely new renderer
|
||||
[self _invalidateRenderer];
|
||||
}
|
||||
|
||||
NSUInteger length = attributedText.length;
|
||||
@@ -495,7 +422,6 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
|
||||
}
|
||||
|
||||
_exclusionPaths = [exclusionPaths copy];
|
||||
[self _invalidateRenderer];
|
||||
[self setNeedsLayout];
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
@@ -536,7 +462,7 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
|
||||
|
||||
CGContextTranslateCTM(context, _textContainerInset.left, _textContainerInset.top);
|
||||
|
||||
ASTextKitRenderer *renderer = [self _rendererWithBounds:drawParameterBounds];
|
||||
ASTextKitRenderer *renderer = [self _rendererWithBoundsSlow:drawParameterBounds];
|
||||
|
||||
// Fill background
|
||||
if (backgroundColor != nil) {
|
||||
@@ -790,11 +716,12 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
|
||||
|
||||
if (highlightTargetLayer != nil) {
|
||||
ASDN::MutexLocker l(__instanceLock__);
|
||||
ASTextKitRenderer *renderer = [self _renderer];
|
||||
|
||||
NSArray *highlightRects = [[self _renderer] rectsForTextRange:highlightRange measureOption:ASTextKitRendererMeasureOptionBlock];
|
||||
NSArray *highlightRects = [renderer rectsForTextRange:highlightRange measureOption:ASTextKitRendererMeasureOptionBlock];
|
||||
NSMutableArray *converted = [NSMutableArray arrayWithCapacity:highlightRects.count];
|
||||
for (NSValue *rectValue in highlightRects) {
|
||||
UIEdgeInsets shadowPadding = _renderer.shadower.shadowPadding;
|
||||
UIEdgeInsets shadowPadding = renderer.shadower.shadowPadding;
|
||||
CGRect rendererRect = ASTextNodeAdjustRenderRectForShadowPadding(rectValue.CGRectValue, shadowPadding);
|
||||
|
||||
// The rects returned from renderer don't have `textContainerInset`,
|
||||
@@ -1119,7 +1046,6 @@ static CGRect ASTextNodeAdjustRenderRectForShadowPadding(CGRect rendererRect, UI
|
||||
CGColorRelease(_shadowColor);
|
||||
_shadowColor = CGColorRetain(shadowColor);
|
||||
_cachedShadowUIColor = [UIColor colorWithCGColor:shadowColor];
|
||||
[self _invalidateRenderer];
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
}
|
||||
@@ -1137,7 +1063,6 @@ static CGRect ASTextNodeAdjustRenderRectForShadowPadding(CGRect rendererRect, UI
|
||||
|
||||
if (!CGSizeEqualToSize(_shadowOffset, shadowOffset)) {
|
||||
_shadowOffset = shadowOffset;
|
||||
[self _invalidateRenderer];
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
}
|
||||
@@ -1155,7 +1080,6 @@ static CGRect ASTextNodeAdjustRenderRectForShadowPadding(CGRect rendererRect, UI
|
||||
|
||||
if (_shadowOpacity != shadowOpacity) {
|
||||
_shadowOpacity = shadowOpacity;
|
||||
[self _invalidateRenderer];
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
}
|
||||
@@ -1173,7 +1097,6 @@ static CGRect ASTextNodeAdjustRenderRectForShadowPadding(CGRect rendererRect, UI
|
||||
|
||||
if (_shadowRadius != shadowRadius) {
|
||||
_shadowRadius = shadowRadius;
|
||||
[self _invalidateRenderer];
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
}
|
||||
@@ -1232,7 +1155,6 @@ static NSAttributedString *DefaultTruncationAttributedString()
|
||||
|
||||
if (_truncationMode != truncationMode) {
|
||||
_truncationMode = truncationMode;
|
||||
[self _invalidateRenderer];
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
}
|
||||
@@ -1251,7 +1173,6 @@ static NSAttributedString *DefaultTruncationAttributedString()
|
||||
|
||||
if ([_pointSizeScaleFactors isEqualToArray:pointSizeScaleFactors] == NO) {
|
||||
_pointSizeScaleFactors = pointSizeScaleFactors;
|
||||
[self _invalidateRenderer];
|
||||
[self setNeedsDisplay];
|
||||
}}
|
||||
|
||||
@@ -1261,7 +1182,6 @@ static NSAttributedString *DefaultTruncationAttributedString()
|
||||
|
||||
if (_maximumNumberOfLines != maximumNumberOfLines) {
|
||||
_maximumNumberOfLines = maximumNumberOfLines;
|
||||
[self _invalidateRenderer];
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
}
|
||||
@@ -1285,7 +1205,6 @@ static NSAttributedString *DefaultTruncationAttributedString()
|
||||
- (void)_invalidateTruncationText
|
||||
{
|
||||
[self _updateComposedTruncationText];
|
||||
[self _invalidateRenderer];
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
|
||||
@@ -475,7 +475,7 @@ static NSString * const kRate = @"rate";
|
||||
ASDN::MutexLocker l(__instanceLock__);
|
||||
|
||||
if (ASObjectIsEqual(assetURL, self.assetURL) == NO) {
|
||||
[self _setAndFetchAsset:[AVURLAsset assetWithURL:assetURL] url:assetURL];
|
||||
[self locked_setAndFetchAsset:[AVURLAsset assetWithURL:assetURL] url:assetURL];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -497,7 +497,7 @@ static NSString * const kRate = @"rate";
|
||||
ASDN::MutexLocker l(__instanceLock__);
|
||||
|
||||
if (ASAssetIsEqual(asset, _asset) == NO) {
|
||||
[self _setAndFetchAsset:asset url:nil];
|
||||
[self locked_setAndFetchAsset:asset url:nil];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -507,9 +507,10 @@ static NSString * const kRate = @"rate";
|
||||
return _asset;
|
||||
}
|
||||
|
||||
- (void)_setAndFetchAsset:(AVAsset *)asset url:(NSURL *)assetURL
|
||||
- (void)locked_setAndFetchAsset:(AVAsset *)asset url:(NSURL *)assetURL
|
||||
{
|
||||
[self didExitPreloadState];
|
||||
self.videoPlaceholderImage = nil;
|
||||
_asset = asset;
|
||||
_assetURL = assetURL;
|
||||
[self setNeedsPreload];
|
||||
|
||||
@@ -47,6 +47,16 @@ static inline NSString * descriptionIndents(NSUInteger indents)
|
||||
*/
|
||||
@property (nonatomic, getter=isFlattened) BOOL flattened;
|
||||
|
||||
/*
|
||||
* Caches all sublayouts if set to YES or destroys the sublayout cache if set to NO. Defaults to YES
|
||||
*/
|
||||
@property (nonatomic, assign) BOOL retainSublayoutLayoutElements;
|
||||
|
||||
/**
|
||||
* Array for explicitly retain sublayout layout elements in case they are created and references in layoutSpecThatFits: and no one else will hold a strong reference on it
|
||||
*/
|
||||
@property (nonatomic, strong) NSMutableArray<id<ASLayoutElement>> *sublayoutLayoutElements;
|
||||
|
||||
@end
|
||||
|
||||
@implementation ASLayout
|
||||
@@ -69,6 +79,7 @@ static inline NSString * descriptionIndents(NSUInteger indents)
|
||||
#endif
|
||||
|
||||
_layoutElement = layoutElement;
|
||||
|
||||
// Read this now to avoid @c weak overhead later.
|
||||
_layoutElementType = layoutElement.layoutElementType;
|
||||
|
||||
@@ -88,7 +99,9 @@ static inline NSString * descriptionIndents(NSUInteger indents)
|
||||
|
||||
_sublayouts = sublayouts != nil ? [sublayouts copy] : @[];
|
||||
_flattened = NO;
|
||||
_retainSublayoutLayoutElements = NO;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@@ -137,6 +150,28 @@ static inline NSString * descriptionIndents(NSUInteger indents)
|
||||
sublayouts:layout.sublayouts];
|
||||
}
|
||||
|
||||
#pragma mark - Sublayout Elements Caching
|
||||
|
||||
- (void)setRetainSublayoutLayoutElements:(BOOL)retainSublayoutLayoutElements
|
||||
{
|
||||
if (_retainSublayoutLayoutElements != retainSublayoutLayoutElements) {
|
||||
_retainSublayoutLayoutElements = retainSublayoutLayoutElements;
|
||||
|
||||
if (retainSublayoutLayoutElements == NO) {
|
||||
_sublayoutLayoutElements = nil;
|
||||
} else {
|
||||
// Add sublayouts layout elements to an internal array to retain it while the layout lives
|
||||
NSUInteger sublayoutCount = _sublayouts.count;
|
||||
if (sublayoutCount > 0) {
|
||||
_sublayoutLayoutElements = [NSMutableArray arrayWithCapacity:sublayoutCount];
|
||||
for (ASLayout *sublayout in _sublayouts) {
|
||||
[_sublayoutLayoutElements addObject:sublayout.layoutElement];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Layout Flattening
|
||||
|
||||
- (ASLayout *)filteredNodeLayoutTree
|
||||
@@ -170,8 +205,10 @@ static inline NSString * descriptionIndents(NSUInteger indents)
|
||||
}
|
||||
queue.insert(queue.cbegin(), sublayoutContexts.begin(), sublayoutContexts.end());
|
||||
}
|
||||
|
||||
return [ASLayout layoutWithLayoutElement:_layoutElement size:_size sublayouts:flattenedSublayouts];
|
||||
|
||||
ASLayout *layout = [ASLayout layoutWithLayoutElement:_layoutElement size:_size position:CGPointZero sublayouts:flattenedSublayouts];
|
||||
layout.retainSublayoutLayoutElements = YES;
|
||||
return layout;
|
||||
}
|
||||
|
||||
#pragma mark - Accessors
|
||||
|
||||
@@ -110,7 +110,8 @@ struct ASTextKitAttributes {
|
||||
&& maximumNumberOfLines == other.maximumNumberOfLines
|
||||
&& shadowOpacity == other.shadowOpacity
|
||||
&& shadowRadius == other.shadowRadius
|
||||
&& [pointSizeScaleFactors isEqualToArray:other.pointSizeScaleFactors]
|
||||
&& (pointSizeScaleFactors == other.pointSizeScaleFactors
|
||||
|| [pointSizeScaleFactors isEqualToArray:other.pointSizeScaleFactors])
|
||||
&& CGSizeEqualToSize(shadowOffset, other.shadowOffset)
|
||||
&& ASObjectIsEqual(exclusionPaths, other.exclusionPaths)
|
||||
&& ASObjectIsEqual(avoidTailTruncationSet, other.avoidTailTruncationSet)
|
||||
|
||||
@@ -29,8 +29,6 @@
|
||||
exclusionPaths:(NSArray *)exclusionPaths
|
||||
constrainedSize:(CGSize)constrainedSize;
|
||||
|
||||
@property (nonatomic, assign, readwrite) CGSize constrainedSize;
|
||||
|
||||
/**
|
||||
All operations on TextKit values MUST occur within this locked context. Simultaneous access (even non-mutative) to
|
||||
TextKit components may cause crashes.
|
||||
|
||||
@@ -54,18 +54,6 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
- (CGSize)constrainedSize
|
||||
{
|
||||
ASDN::MutexSharedLocker l(__instanceLock__);
|
||||
return _textContainer.size;
|
||||
}
|
||||
|
||||
- (void)setConstrainedSize:(CGSize)constrainedSize
|
||||
{
|
||||
ASDN::MutexSharedLocker l(__instanceLock__);
|
||||
_textContainer.size = constrainedSize;
|
||||
}
|
||||
|
||||
- (void)performBlockWithLockedTextKitComponents:(void (^)(NSLayoutManager *,
|
||||
NSTextStorage *,
|
||||
NSTextContainer *))block
|
||||
|
||||
@@ -37,7 +37,6 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet()
|
||||
|
||||
@implementation ASTextKitRenderer {
|
||||
CGSize _calculatedSize;
|
||||
BOOL _sizeIsCalculated;
|
||||
}
|
||||
@synthesize attributes = _attributes, context = _context, shadower = _shadower, truncater = _truncater, fontSizeAdjuster = _fontSizeAdjuster;
|
||||
|
||||
@@ -49,62 +48,38 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet()
|
||||
if (self = [super init]) {
|
||||
_constrainedSize = constrainedSize;
|
||||
_attributes = attributes;
|
||||
_sizeIsCalculated = NO;
|
||||
_currentScaleFactor = 1;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (ASTextKitShadower *)shadower
|
||||
{
|
||||
if (!_shadower) {
|
||||
ASTextKitAttributes attributes = _attributes;
|
||||
|
||||
// As the renderer should be thread safe, create all subcomponents in the initialization method
|
||||
_shadower = [ASTextKitShadower shadowerWithShadowOffset:attributes.shadowOffset
|
||||
shadowColor:attributes.shadowColor
|
||||
shadowOpacity:attributes.shadowOpacity
|
||||
shadowRadius:attributes.shadowRadius];
|
||||
}
|
||||
return _shadower;
|
||||
}
|
||||
|
||||
- (ASTextKitTailTruncater *)truncater
|
||||
{
|
||||
if (!_truncater) {
|
||||
ASTextKitAttributes attributes = _attributes;
|
||||
NSCharacterSet *avoidTailTruncationSet = attributes.avoidTailTruncationSet ? : _defaultAvoidTruncationCharacterSet();
|
||||
_truncater = [[ASTextKitTailTruncater alloc] initWithContext:[self context]
|
||||
truncationAttributedString:attributes.truncationAttributedString
|
||||
avoidTailTruncationSet:avoidTailTruncationSet];
|
||||
}
|
||||
return _truncater;
|
||||
}
|
||||
|
||||
- (ASTextKitFontSizeAdjuster *)fontSizeAdjuster
|
||||
{
|
||||
if (!_fontSizeAdjuster) {
|
||||
ASTextKitAttributes attributes = _attributes;
|
||||
// We must inset the constrained size by the size of the shadower.
|
||||
CGSize shadowConstrainedSize = [[self shadower] insetSizeWithConstrainedSize:_constrainedSize];
|
||||
_fontSizeAdjuster = [[ASTextKitFontSizeAdjuster alloc] initWithContext:[self context]
|
||||
constrainedSize:shadowConstrainedSize
|
||||
textKitAttributes:attributes];
|
||||
}
|
||||
return _fontSizeAdjuster;
|
||||
}
|
||||
|
||||
- (ASTextKitContext *)context
|
||||
{
|
||||
if (!_context) {
|
||||
ASTextKitAttributes attributes = _attributes;
|
||||
|
||||
// We must inset the constrained size by the size of the shadower.
|
||||
CGSize shadowConstrainedSize = [[self shadower] insetSizeWithConstrainedSize:_constrainedSize];
|
||||
|
||||
_context = [[ASTextKitContext alloc] initWithAttributedString:attributes.attributedString
|
||||
lineBreakMode:attributes.lineBreakMode
|
||||
maximumNumberOfLines:attributes.maximumNumberOfLines
|
||||
exclusionPaths:attributes.exclusionPaths
|
||||
constrainedSize:shadowConstrainedSize];
|
||||
|
||||
NSCharacterSet *avoidTailTruncationSet = attributes.avoidTailTruncationSet ?: _defaultAvoidTruncationCharacterSet();
|
||||
_truncater = [[ASTextKitTailTruncater alloc] initWithContext:[self context]
|
||||
truncationAttributedString:attributes.truncationAttributedString
|
||||
avoidTailTruncationSet:avoidTailTruncationSet];
|
||||
|
||||
ASTextKitAttributes attributes = _attributes;
|
||||
// We must inset the constrained size by the size of the shadower.
|
||||
_fontSizeAdjuster = [[ASTextKitFontSizeAdjuster alloc] initWithContext:[self context]
|
||||
constrainedSize:shadowConstrainedSize
|
||||
textKitAttributes:attributes];
|
||||
|
||||
// Calcualate size immediately
|
||||
[self _calculateSize];
|
||||
}
|
||||
return _context;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSStringDrawingContext *)stringDrawingContext
|
||||
@@ -127,10 +102,6 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet()
|
||||
|
||||
- (CGSize)size
|
||||
{
|
||||
if (!_sizeIsCalculated) {
|
||||
[self _calculateSize];
|
||||
_sizeIsCalculated = YES;
|
||||
}
|
||||
return _calculatedSize;
|
||||
}
|
||||
|
||||
@@ -222,12 +193,6 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet()
|
||||
{
|
||||
// We add an assertion so we can track the rare conditions where a graphics context is not present
|
||||
ASDisplayNodeAssertNotNil(context, @"This is no good without a context.");
|
||||
|
||||
// This renderer may not be the one that did the sizing. If that is the case its truncation and currentScaleFactor may not have been evaluated.
|
||||
// If there's any possibility we need to truncate or scale (i.e. width is not infinite), perform the size calculation.
|
||||
if (_sizeIsCalculated == NO && isinf(_constrainedSize.width) == NO) {
|
||||
[self _calculateSize];
|
||||
}
|
||||
|
||||
bounds = CGRectIntersection(bounds, { .size = _constrainedSize });
|
||||
CGRect shadowInsetBounds = [[self shadower] insetRectWithConstrainedRect:bounds];
|
||||
@@ -298,9 +263,7 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet()
|
||||
|
||||
- (std::vector<NSRange>)visibleRanges
|
||||
{
|
||||
ASTextKitTailTruncater *truncater = [self truncater];
|
||||
[truncater truncate];
|
||||
return truncater.visibleRanges;
|
||||
return _truncater.visibleRanges;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -121,4 +121,54 @@
|
||||
XCTAssertThrows([ASLayout layoutWithLayoutElement:displayNode size:CGSizeMake(INFINITY, INFINITY)]);
|
||||
}
|
||||
|
||||
- (void)testThatLayoutElementCreatedInLayoutSpecThatFitsDoNotGetDeallocated
|
||||
{
|
||||
const CGSize kSize = CGSizeMake(300, 300);
|
||||
|
||||
ASDisplayNode *subNode = [[ASDisplayNode alloc] init];
|
||||
subNode.automaticallyManagesSubnodes = YES;
|
||||
subNode.layoutSpecBlock = ^(ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) {
|
||||
ASTextNode *textNode = [ASTextNode new];
|
||||
textNode.attributedText = [[NSAttributedString alloc] initWithString:@"Test Test Test Test Test Test Test Test"];
|
||||
ASInsetLayoutSpec *insetSpec = [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:textNode];
|
||||
return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:insetSpec];
|
||||
};
|
||||
|
||||
ASDisplayNode *rootNode = [[ASDisplayNode alloc] init];
|
||||
rootNode.automaticallyManagesSubnodes = YES;
|
||||
rootNode.layoutSpecBlock = ^(ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) {
|
||||
ASTextNode *textNode = [ASTextNode new];
|
||||
textNode.attributedText = [[NSAttributedString alloc] initWithString:@"Test Test Test Test Test"];
|
||||
ASInsetLayoutSpec *insetSpec = [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:textNode];
|
||||
|
||||
return [ASStackLayoutSpec
|
||||
stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical
|
||||
spacing:0.0
|
||||
justifyContent:ASStackLayoutJustifyContentStart
|
||||
alignItems:ASStackLayoutAlignItemsStretch
|
||||
children:@[insetSpec, subNode]];
|
||||
};
|
||||
|
||||
rootNode.frame = CGRectMake(0, 0, kSize.width, kSize.height);
|
||||
[rootNode view];
|
||||
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Execute measure and layout pass"];
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
|
||||
[rootNode layoutThatFits:ASSizeRangeMake(kSize)];
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
XCTAssertNoThrow([rootNode.view layoutIfNeeded]);
|
||||
[expectation fulfill];
|
||||
});
|
||||
});
|
||||
|
||||
[self waitForExpectationsWithTimeout:5.0 handler:^(NSError *error) {
|
||||
if (error) {
|
||||
XCTFail(@"Expectation failed: %@", error);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -302,4 +302,10 @@
|
||||
[mockDelegate verify];
|
||||
}
|
||||
|
||||
@end
|
||||
- (void)testThatSettingAnImageExternallyWillThrow
|
||||
{
|
||||
ASMultiplexImageNode *multiplexImageNode = [[ASMultiplexImageNode alloc] init];
|
||||
XCTAssertThrows(multiplexImageNode.image = [UIImage imageNamed:@""]);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -71,6 +71,17 @@
|
||||
[downloader verifyWithDelay:5];
|
||||
}
|
||||
|
||||
- (void)testThatSettingAnImageWillStayForEnteringAndExitingPreloadState
|
||||
{
|
||||
UIImage *image = [[UIImage alloc] init];
|
||||
ASNetworkImageNode *networkImageNode = [[ASNetworkImageNode alloc] init];
|
||||
networkImageNode.image = image;
|
||||
[networkImageNode enterInterfaceState:ASInterfaceStatePreload];
|
||||
XCTAssertEqualObjects(image, networkImageNode.image);
|
||||
[networkImageNode exitInterfaceState:ASInterfaceStatePreload];
|
||||
XCTAssertEqualObjects(image, networkImageNode.image);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation ASTestImageCache
|
||||
|
||||
@@ -18,6 +18,13 @@
|
||||
|
||||
@implementation ASTextNodeSnapshotTests
|
||||
|
||||
- (void)setUp
|
||||
{
|
||||
[super setUp];
|
||||
|
||||
self.recordMode = NO;
|
||||
}
|
||||
|
||||
- (void)testTextContainerInset
|
||||
{
|
||||
// trivial test case to ensure ASSnapshotTestCase works
|
||||
|
||||
@@ -171,7 +171,9 @@
|
||||
ASTextNodeTestDelegate *delegate = [ASTextNodeTestDelegate new];
|
||||
_textNode.delegate = delegate;
|
||||
|
||||
[_textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(100, 100))];
|
||||
ASLayout *layout = [_textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(100, 100))];
|
||||
_textNode.frame = CGRectMake(0, 0, layout.size.width, layout.size.height);
|
||||
|
||||
NSRange returnedLinkRange;
|
||||
NSString *returnedAttributeName;
|
||||
NSString *returnedLinkAttributeValue = [_textNode linkAttributeValueAtPoint:CGPointMake(3, 3) attributeName:&returnedAttributeName range:&returnedLinkRange];
|
||||
|
||||
@@ -410,7 +410,6 @@
|
||||
[_videoNode didExitPreloadState];
|
||||
XCTAssertNil(_videoNode.player);
|
||||
XCTAssertNil(_videoNode.currentItem);
|
||||
XCTAssertNil(_videoNode.image);
|
||||
}
|
||||
|
||||
- (void)testDelegateProperlySetForClassHierarchy
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.8 KiB |
Reference in New Issue
Block a user