Files
ManagerDemo/Pods/DTCoreText/Core/Source/DTAttributedTextContentView.m
T
2014-08-08 13:27:54 +08:00

1091 lines
31 KiB
Objective-C

//
// DTAttributedTextContentView.h
// DTCoreText
//
// Created by Oliver Drobnik on 1/9/11.
// Copyright 2011 Drobnik.com. All rights reserved.
//
#import <QuartzCore/QuartzCore.h>
#import "DTCoreText.h"
#import "DTAttributedTextContentView.h"
#import "DTDictationPlaceholderTextAttachment.h"
#import "DTAccessibilityViewProxy.h"
#import "DTAccessibilityElement.h"
#import "DTCoreTextLayoutFrameAccessibilityElementGenerator.h"
#import "DTBlockFunctions.h"
#if !__has_feature(objc_arc)
#error THIS CODE MUST BE COMPILED WITH ARC ENABLED!
#endif
NSString * const DTAttributedTextContentViewDidFinishLayoutNotification = @"DTAttributedTextContentViewDidFinishLayoutNotification";
@interface DTAttributedTextContentView () <DTAccessibilityViewProxyDelegate>
{
BOOL _shouldAddFirstLineLeading;
BOOL _shouldDrawImages;
BOOL _shouldDrawLinks;
BOOL _shouldLayoutCustomSubviews;
DTAttributedTextContentViewRelayoutMask _relayoutMask;
NSMutableSet *customViews;
NSMutableDictionary *customViewsForLinksIndex;
BOOL _isTiling;
BOOL _layoutFrameHeightIsConstrainedByBounds;
DTCoreTextLayouter *_layouter;
CGPoint _layoutOffset;
CGSize _backgroundOffset;
// lookup bitmask what delegate methods are implemented
struct
{
unsigned int delegateSupportsCustomViewsForAttachments:1;
unsigned int delegateSupportsCustomViewsForLinks:1;
unsigned int delegateSupportsGenericCustomViews:1;
unsigned int delegateSupportsNotificationBeforeDrawing:1;
unsigned int delegateSupportsNotificationAfterDrawing:1;
unsigned int delegateSupportsNotificationBeforeTextBoxDrawing:1;
} _delegateFlags;
DT_WEAK_VARIABLE id <DTAttributedTextContentViewDelegate> _delegate;
}
@property (nonatomic, strong) NSMutableDictionary *customViewsForLinksIndex;
@property (nonatomic, strong) NSMutableDictionary *customViewsForAttachmentsIndex;
@property (nonatomic, strong) NSMutableSet *customViews;
@property (nonatomic, strong) NSArray *accessibilityElements;
- (void)removeAllCustomViews;
- (void)removeAllCustomViewsForLinks;
- (void)removeSubviewsOutsideRect:(CGRect)rect;
@end
static Class _layerClassToUseForDTAttributedTextContentView = nil;
@implementation DTAttributedTextContentView (Tiling)
+ (void)setLayerClass:(Class)layerClass
{
_layerClassToUseForDTAttributedTextContentView = layerClass;
}
+ (Class)layerClass
{
if (_layerClassToUseForDTAttributedTextContentView)
{
return _layerClassToUseForDTAttributedTextContentView;
}
return [CALayer class];
}
@end
@implementation DTAttributedTextContentView (Cursor)
- (NSInteger)closestCursorIndexToPoint:(CGPoint)point
{
return [self.layoutFrame closestCursorIndexToPoint:point];
}
- (CGRect)cursorRectAtIndex:(NSInteger)index
{
return [self.layoutFrame cursorRectAtIndex:index];
}
@end
@implementation DTAttributedTextContentView
- (void)setup
{
self.contentMode = UIViewContentModeTopLeft; // to avoid bitmap scaling effect on resize
_shouldLayoutCustomSubviews = YES;
// no extra leading is added by default
_shouldAddFirstLineLeading = NO;
// by default we draw images, if custom views are supported (by setting delegate) this is disabled
// if you still want images to be drawn together with text then set it back to YES after setting delegate
_shouldDrawImages = YES;
// by default we draw links. If you don't want that because you want to highlight the text in
// DTLinkButton set this property to NO and create a highlighted version of the attributed string
_shouldDrawLinks = YES;
_layoutFrameHeightIsConstrainedByBounds = NO; // we calculate the necessary height unemcumbered by bounds
_relayoutMask = DTAttributedTextContentViewRelayoutOnWidthChanged;
// possibly already set in NIB
if (!self.backgroundColor)
{
self.backgroundColor = [DTColor whiteColor];
}
// set tile size if applicable
CATiledLayer *layer = (id)self.layer;
if ([layer isKindOfClass:[CATiledLayer class]])
{
// get larger dimension and multiply by scale
UIScreen *mainScreen = [UIScreen mainScreen];
CGFloat largerDimension = MAX(mainScreen.applicationFrame.size.width, mainScreen.applicationFrame.size.height);
CGFloat scale = mainScreen.scale;
// this way tiles cover entire screen regardless of orientation or scale
CGSize tileSize = CGSizeMake(largerDimension * scale, largerDimension * scale);
layer.tileSize = tileSize;
_isTiling = YES;
}
}
- (id)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame]))
{
[self setup];
}
return self;
}
- (void)awakeFromNib
{
[self setup];
}
- (void)dealloc
{
[self removeAllCustomViews];
}
- (NSString *)debugDescription
{
NSString *extract = [[[_layoutFrame attributedStringFragment] string] substringFromIndex:[self.layoutFrame visibleStringRange].location];
if ([extract length]>10)
{
extract = [extract substringToIndex:10];
}
return [NSString stringWithFormat:@"<%@ %@ range:%@ '%@...'>", [self class], NSStringFromCGRect(self.frame),NSStringFromRange([self.layoutFrame visibleStringRange]), extract];
}
- (void)layoutSubviewsInRect:(CGRect)rect
{
// if we are called for partial (non-infinate) we remove unneeded custom subviews first
if (!CGRectIsInfinite(rect))
{
[self removeSubviewsOutsideRect:rect];
}
[CATransaction begin];
[CATransaction setDisableActions:YES];
DTCoreTextLayoutFrame *theLayoutFrame = self.layoutFrame;
NSAttributedString *layoutString = [theLayoutFrame attributedStringFragment];
NSArray *lines;
if (CGRectIsInfinite(rect))
{
lines = [theLayoutFrame lines];
}
else
{
lines = [theLayoutFrame linesVisibleInRect:rect];
}
// hide all customViews
for (UIView *view in self.customViews)
{
view.hidden = YES;
}
for (DTCoreTextLayoutLine *oneLine in lines)
{
NSUInteger skipRunsBeforeLocation = 0;
for (DTCoreTextGlyphRun *oneRun in oneLine.glyphRuns)
{
// add custom views if necessary
NSRange runRange = [oneRun stringRange];
CGRect frameForSubview = CGRectZero;
if (runRange.location>=skipRunsBeforeLocation)
{
// see if it's a link
NSRange effectiveRangeOfLink;
// make sure that a link is only as long as the area to the next attachment or the current attachment itself
DTTextAttachment *attachment = oneRun.attributes[NSAttachmentAttributeName];
// if there is no attachment then the effectiveRangeOfAttachment contains the range until the next attachment
NSURL *linkURL = oneRun.attributes[DTLinkAttribute];
if (linkURL)
{
effectiveRangeOfLink = runRange;
}
// avoid chaining together glyph runs for an attachment
if (linkURL && !attachment)
{
NSInteger glyphIndex = [oneLine.glyphRuns indexOfObject:oneRun];
// chain together this glyph run with following runs for combined link buttons
for (NSInteger i=glyphIndex+1; i<[oneLine.glyphRuns count]; i++)
{
DTCoreTextGlyphRun *followingRun = oneLine.glyphRuns[i];
NSURL *followingURL = followingRun.attributes[DTLinkAttribute];
if (![[followingURL absoluteString] isEqualToString:[linkURL absoluteString]])
{
// following glyph run has different or no link URL
break;
}
if (followingRun.attachment)
{
// do not join a following attachment to the combined link
break;
}
// extend effective link range
effectiveRangeOfLink = NSUnionRange(effectiveRangeOfLink, followingRun.stringRange);
}
// frame for link view includes for all joined glyphruns with same link in this line
frameForSubview = [oneLine frameOfGlyphsWithRange:effectiveRangeOfLink];
// this following glyph run link attribute is joined to link range
skipRunsBeforeLocation = NSMaxRange(effectiveRangeOfLink);
}
else
{
// individual glyph run
if (attachment)
{
// frame might be different due to image vertical alignment
CGFloat ascender = [attachment ascentForLayout];
CGFloat descender = [attachment descentForLayout];
frameForSubview = CGRectMake(oneRun.frame.origin.x, oneLine.baselineOrigin.y - ascender, oneRun.frame.size.width, ascender+descender);
}
else
{
frameForSubview = oneRun.frame;
}
}
// if there is an attachment then we continue even with empty frame, might be a lazily loaded image
if ((frameForSubview.size.width<=0 || frameForSubview.size.height<=0) && !attachment)
{
continue;
}
NSNumber *indexKey = [NSNumber numberWithInteger:runRange.location];
// offset layout if necessary
if (!CGPointEqualToPoint(_layoutOffset, CGPointZero))
{
frameForSubview.origin.x += _layoutOffset.x;
frameForSubview.origin.y += _layoutOffset.y;
}
// round frame
frameForSubview.origin.x = floor(frameForSubview.origin.x);
frameForSubview.origin.y = ceil(frameForSubview.origin.y);
frameForSubview.size.width = round(frameForSubview.size.width);
frameForSubview.size.height = round(frameForSubview.size.height);
if (CGRectGetMinY(frameForSubview)> CGRectGetMaxY(rect) || CGRectGetMaxY(frameForSubview) < CGRectGetMinY(rect))
{
// is still outside even though the bounds of the line already intersect visible area
continue;
}
if (attachment)
{
indexKey = [NSNumber numberWithInteger:[attachment hash]];
UIView *existingAttachmentView = [self.customViewsForAttachmentsIndex objectForKey:indexKey];
if (existingAttachmentView)
{
existingAttachmentView.hidden = NO;
existingAttachmentView.frame = frameForSubview;
existingAttachmentView.alpha = 1;
[existingAttachmentView setNeedsLayout];
[existingAttachmentView setNeedsDisplay];
linkURL = nil; // prevent adding link button on top of image view
}
else
{
UIView *newCustomAttachmentView = nil;
if ([attachment isKindOfClass:[DTDictationPlaceholderTextAttachment class]])
{
newCustomAttachmentView = [DTDictationPlaceholderView placeholderView];
newCustomAttachmentView.frame = frameForSubview; // set fixed frame
}
else if (_delegateFlags.delegateSupportsCustomViewsForAttachments)
{
newCustomAttachmentView = [_delegate attributedTextContentView:self viewForAttachment:attachment frame:frameForSubview];
}
else if (_delegateFlags.delegateSupportsGenericCustomViews)
{
NSAttributedString *string = [layoutString attributedSubstringFromRange:runRange];
newCustomAttachmentView = [_delegate attributedTextContentView:self viewForAttributedString:string frame:frameForSubview];
}
if (newCustomAttachmentView)
{
// delegate responsible to set frame
if (newCustomAttachmentView)
{
newCustomAttachmentView.tag = [indexKey integerValue];
[self addSubview:newCustomAttachmentView];
[self.customViews addObject:newCustomAttachmentView];
[self.customViewsForAttachmentsIndex setObject:newCustomAttachmentView forKey:indexKey];
linkURL = nil; // prevent adding link button on top of image view
}
}
}
}
if (linkURL && (_delegateFlags.delegateSupportsCustomViewsForLinks || _delegateFlags.delegateSupportsGenericCustomViews))
{
UIView *existingLinkView = [self.customViewsForLinksIndex objectForKey:indexKey];
// make sure that the frame height is no less than the line height for hyperlinks
if (frameForSubview.size.height < oneLine.frame.size.height)
{
frameForSubview.origin.y = trunc(oneLine.frame.origin.y);
frameForSubview.size.height = ceil(oneLine.frame.size.height);
}
if (existingLinkView)
{
existingLinkView.frame = frameForSubview;
existingLinkView.hidden = NO;
}
else
{
UIView *newCustomLinkView = nil;
// make sure that the frame height is no less than the line height for hyperlinks
if (frameForSubview.size.height < oneLine.frame.size.height)
{
frameForSubview.origin.y = trunc(oneLine.frame.origin.y);
frameForSubview.size.height = ceil(oneLine.frame.size.height);
}
if (_delegateFlags.delegateSupportsCustomViewsForLinks)
{
NSDictionary *attributes = [layoutString attributesAtIndex:runRange.location effectiveRange:NULL];
NSString *guid = [attributes objectForKey:DTGUIDAttribute];
newCustomLinkView = [_delegate attributedTextContentView:self viewForLink:linkURL identifier:guid frame:frameForSubview];
}
else if (_delegateFlags.delegateSupportsGenericCustomViews)
{
NSAttributedString *string = [layoutString attributedSubstringFromRange:runRange];
newCustomLinkView = [_delegate attributedTextContentView:self viewForAttributedString:string frame:frameForSubview];
}
// delegate responsible to set frame
if (newCustomLinkView)
{
newCustomLinkView.tag = runRange.location;
[self addSubview:newCustomLinkView];
[self.customViews addObject:newCustomLinkView];
[self.customViewsForLinksIndex setObject:newCustomLinkView forKey:indexKey];
}
}
}
}
}
}
[CATransaction commit];
}
- (void)layoutSubviews
{
[super layoutSubviews];
if (!_isTiling && (self.bounds.size.width>1024.0 || self.bounds.size.height>1024.0))
{
if (![self.layer isKindOfClass:[CATiledLayer class]])
{
NSLog(@"Warning: A %@ with size %@ is using a non-tiled layer. Set the layer class to a CATiledLayer subclass with [DTAttributedTextContentView setLayerClass:[DTTiledLayerWithoutFade class]].", NSStringFromClass([self class]), NSStringFromCGSize(self.bounds.size));
}
}
if (_shouldLayoutCustomSubviews)
{
[self layoutSubviewsInRect:CGRectInfinite];
}
}
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
// needs clearing of background
CGRect rect = CGContextGetClipBoundingBox(ctx);
if (_backgroundOffset.height || _backgroundOffset.width)
{
CGContextSetPatternPhase(ctx, _backgroundOffset);
}
CGContextSetFillColorWithColor(ctx, [self.backgroundColor CGColor]);
CGContextFillRect(ctx, rect);
// offset layout if necessary
if (!CGPointEqualToPoint(_layoutOffset, CGPointZero))
{
CGAffineTransform transform = CGAffineTransformMakeTranslation(_layoutOffset.x, _layoutOffset.y);
CGContextConcatCTM(ctx, transform);
}
DTCoreTextLayoutFrame *theLayoutFrame = self.layoutFrame; // this is synchronized
// construct drawing options
DTCoreTextLayoutFrameDrawingOptions options = DTCoreTextLayoutFrameDrawingDefault;
if (!_shouldDrawImages)
{
options |= DTCoreTextLayoutFrameDrawingOmitAttachments;
}
if (!_shouldDrawLinks)
{
options |= DTCoreTextLayoutFrameDrawingOmitLinks;
}
if (_delegateFlags.delegateSupportsNotificationBeforeDrawing)
{
[_delegate attributedTextContentView:self willDrawLayoutFrame:theLayoutFrame inContext:ctx];
}
// need to prevent updating of string and drawing at the same time
[theLayoutFrame drawInContext:ctx options:options];
if (_delegateFlags.delegateSupportsNotificationAfterDrawing)
{
[_delegate attributedTextContentView:self didDrawLayoutFrame:theLayoutFrame inContext:ctx];
}
}
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
[self.layoutFrame drawInContext:context options:DTCoreTextLayoutFrameDrawingDefault];
}
- (void)relayoutText
{
DTBlockPerformSyncIfOnMainThreadElseAsync(^{
// Make sure we actually have a superview and a previous layout before attempting to relayout the text.
if (_layoutFrame && self.superview)
{
// need new layout frame, layouter can remain because the attributed string is probably the same
self.layoutFrame = nil;
// remove all links because they might have merged or split
[self removeAllCustomViewsForLinks];
if (_attributedString)
{
// triggers new layout
CGSize neededSize = [self intrinsicContentSize];
CGRect optimalFrame = CGRectMake(self.frame.origin.x, self.frame.origin.y, neededSize.width, neededSize.height);
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:[NSValue valueWithCGRect:optimalFrame] forKey:@"OptimalFrame"];
[[NSNotificationCenter defaultCenter] postNotificationName:DTAttributedTextContentViewDidFinishLayoutNotification object:self userInfo:userInfo];
}
[self setNeedsLayout];
[self setNeedsDisplayInRect:self.bounds];
if ([self respondsToSelector:@selector(invalidateIntrinsicContentSize)])
{
[self invalidateIntrinsicContentSize];
}
}
});
}
- (void)removeAllCustomViewsForLinks
{
NSArray *linkViews = [customViewsForLinksIndex allValues];
for (UIView *customView in linkViews)
{
[customView removeFromSuperview];
[customViews removeObject:customView];
}
[customViewsForLinksIndex removeAllObjects];
}
- (void)removeAllCustomViews
{
NSSet *allCustomViews = [NSSet setWithSet:customViews];
for (UIView *customView in allCustomViews)
{
[customView removeFromSuperview];
[customViews removeObject:customView];
}
[customViewsForAttachmentsIndex removeAllObjects];
[customViewsForLinksIndex removeAllObjects];
}
- (void)removeSubviewsOutsideRect:(CGRect)rect
{
NSSet *allCustomViews = [NSSet setWithSet:customViews];
for (UIView *customView in allCustomViews)
{
if (CGRectGetMinY(customView.frame)> CGRectGetMaxY(rect) || CGRectGetMaxY(customView.frame) < CGRectGetMinY(rect))
{
NSNumber *indexKey = [NSNumber numberWithInteger:customView.tag];
[customView removeFromSuperview];
[customViews removeObject:customView];
[customViewsForAttachmentsIndex removeObjectForKey:indexKey];
[customViewsForLinksIndex removeObjectForKey:indexKey];
}
}
}
#pragma mark - Sizing
- (CGSize)intrinsicContentSize
{
if (!self.layoutFrame) // creates new layout frame if possible
{
return CGSizeMake(-1, -1); // UIViewNoIntrinsicMetric as of iOS 6
}
// we have a layout frame and from this we get the needed size
return CGSizeMake(_layoutFrame.frame.size.width + _edgeInsets.left + _edgeInsets.right, CGRectGetMaxY(_layoutFrame.frame) + _edgeInsets.bottom);
}
- (CGSize)sizeThatFits:(CGSize)size
{
CGSize neededSize = [self intrinsicContentSize]; // creates layout frame if necessary
if (neededSize.width>=0 && neededSize.height>=0)
{
return neededSize;
}
// return empty size plus padding
return CGSizeMake(_edgeInsets.left + _edgeInsets.right, _edgeInsets.bottom + _edgeInsets.top);
}
- (CGRect)_frameForLayoutFrameConstraintedToWidth:(CGFloat)constrainWidth
{
if (!isnormal(constrainWidth))
{
constrainWidth = self.bounds.size.width;
}
CGRect bounds = self.bounds;
bounds.size.width = constrainWidth;
CGRect rect = UIEdgeInsetsInsetRect(bounds, _edgeInsets);
if (rect.size.width<=0)
{
// cannot create layout frame with negative or zero width
return CGRectZero;
}
if (_layoutFrameHeightIsConstrainedByBounds)
{
if (rect.size.height<=0)
{
// cannot create layout frame with negative or zero height if flexible height is disabled
return CGRectZero;
}
// already set height to bounds height
}
else
{
rect.size.height = CGFLOAT_HEIGHT_UNKNOWN; // necessary height set as soon as we know it.
}
return rect;
}
- (CGSize)suggestedFrameSizeToFitEntireStringConstraintedToWidth:(CGFloat)width
{
// create a temporary frame, will be cached by the layouter for the given rect
CGRect rect = [self _frameForLayoutFrameConstraintedToWidth:width];
DTCoreTextLayoutFrame *tmpLayoutFrame = [self.layouter layoutFrameWithRect:rect range:NSMakeRange(0, 0)];
// assign current layout frame properties to tmpLayoutFrame
tmpLayoutFrame.numberOfLines = _numberOfLines;
tmpLayoutFrame.lineBreakMode = _lineBreakMode;
tmpLayoutFrame.truncationString = _truncationString;
// we have a layout frame and from this we get the needed size
return CGSizeMake(tmpLayoutFrame.frame.size.width + _edgeInsets.left + _edgeInsets.right, CGRectGetMaxY(tmpLayoutFrame.frame) + _edgeInsets.bottom);
}
#pragma mark Properties
- (void)setEdgeInsets:(UIEdgeInsets)edgeInsets
{
if (!UIEdgeInsetsEqualToEdgeInsets(edgeInsets, _edgeInsets))
{
_edgeInsets = edgeInsets;
[self relayoutText];
if ([self respondsToSelector:@selector(invalidateIntrinsicContentSize)])
{
[self invalidateIntrinsicContentSize];
}
}
}
- (void)setAttributedString:(NSAttributedString *)string
{
if (_attributedString != string)
{
// keep the layouter, update string
self.layouter.attributedString = string;
_attributedString = [string copy];
// only do relayout if there is a previous layout frame and visible
if (_layoutFrame)
{
// new layout invalidates all positions for custom views
[self removeAllCustomViews];
// relayout only occurs if the view is visible
[self relayoutText];
}
else
{
// this is needed or else no lazy layout will be triggered if there is no layout frame yet (before this is added to a superview)
[self setNeedsLayout];
[self setNeedsDisplayInRect:self.bounds];
}
if ([self respondsToSelector:@selector(invalidateIntrinsicContentSize)])
{
[self invalidateIntrinsicContentSize];
}
}
}
- (void)setFrame:(CGRect)frame
{
CGRect oldFrame = self.frame;
[super setFrame:frame];
if (!_layoutFrame)
{
return;
}
// having a layouter means we are responsible for layouting yourselves
// relayout based on relayoutMask
BOOL shouldRelayout = NO;
if (_relayoutMask & DTAttributedTextContentViewRelayoutOnHeightChanged)
{
if (oldFrame.size.height != frame.size.height)
{
shouldRelayout = YES;
}
}
if (_relayoutMask & DTAttributedTextContentViewRelayoutOnWidthChanged)
{
if (oldFrame.size.width != frame.size.width)
{
shouldRelayout = YES;
}
}
if (shouldRelayout)
{
[self relayoutText];
}
if (oldFrame.size.height < frame.size.height)
{
// need to draw the newly visible area
[self setNeedsDisplayInRect:CGRectMake(0, oldFrame.size.height, self.bounds.size.width, frame.size.height - oldFrame.size.height)];
}
}
- (void)setShouldAddFirstLineLeading:(BOOL)shouldAddLeading
{
if (_shouldAddFirstLineLeading != shouldAddLeading)
{
_shouldAddFirstLineLeading = shouldAddLeading;
[self setNeedsDisplay];
if ([self respondsToSelector:@selector(invalidateIntrinsicContentSize)])
{
[self invalidateIntrinsicContentSize];
}
}
}
- (void)setShouldDrawImages:(BOOL)shouldDrawImages
{
if (_shouldDrawImages != shouldDrawImages)
{
_shouldDrawImages = shouldDrawImages;
[self setNeedsDisplay];
if ([self respondsToSelector:@selector(invalidateIntrinsicContentSize)])
{
[self invalidateIntrinsicContentSize];
}
}
}
- (void)setShouldDrawLinks:(BOOL)shouldDrawLinks
{
if (_shouldDrawLinks != shouldDrawLinks)
{
_shouldDrawLinks = shouldDrawLinks;
[self setNeedsDisplay];
if ([self respondsToSelector:@selector(invalidateIntrinsicContentSize)])
{
[self invalidateIntrinsicContentSize];
}
}
}
- (void)setBackgroundColor:(DTColor *)newColor
{
super.backgroundColor = newColor;
if ([newColor alphaComponent]<1.0)
{
self.opaque = NO;
}
else
{
self.opaque = YES;
}
}
- (DTCoreTextLayouter *)layouter
{
@synchronized(self)
{
if (!_layouter)
{
if (_attributedString)
{
_layouter = [[DTCoreTextLayouter alloc] initWithAttributedString:_attributedString];
// allow frame caching if somebody uses the suggestedSize
_layouter.shouldCacheLayoutFrames = YES;
}
}
return _layouter;
}
}
- (void)setLayouter:(DTCoreTextLayouter *)layouter
{
@synchronized(self)
{
if (_layouter != layouter)
{
_layouter = layouter;
}
}
}
- (DTCoreTextLayoutFrame *)layoutFrame
{
@synchronized(self)
{
DTCoreTextLayouter *theLayouter = self.layouter;
if (!_layoutFrame)
{
// we can only layout if we have our own layouter
if (theLayouter)
{
CGRect rect = UIEdgeInsetsInsetRect(self.bounds, _edgeInsets);
if (rect.size.width<=0)
{
// cannot create layout frame with negative or zero width
return nil;
}
if (_layoutFrameHeightIsConstrainedByBounds)
{
if (rect.size.height<=0)
{
// cannot create layout frame with negative or zero height if flexible height is disabled
return nil;
}
// height already set
}
else
{
rect.size.height = CGFLOAT_HEIGHT_UNKNOWN; // necessary height set as soon as we know it.
}
_layoutFrame = [theLayouter layoutFrameWithRect:rect range:NSMakeRange(0, 0)];
_layoutFrame.numberOfLines = _numberOfLines;
_layoutFrame.lineBreakMode = _lineBreakMode;
_layoutFrame.truncationString = _truncationString;
// this must have been the initial layout pass
CGSize neededSize = CGSizeMake(_layoutFrame.frame.size.width + _edgeInsets.left + _edgeInsets.right, CGRectGetMaxY(_layoutFrame.frame) + _edgeInsets.bottom);
CGRect optimalFrame = CGRectMake(self.frame.origin.x, self.frame.origin.y, neededSize.width, neededSize.height);
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:[NSValue valueWithCGRect:optimalFrame] forKey:@"OptimalFrame"];
[[NSNotificationCenter defaultCenter] postNotificationName:DTAttributedTextContentViewDidFinishLayoutNotification object:self userInfo:userInfo];
if (_delegateFlags.delegateSupportsNotificationBeforeTextBoxDrawing)
{
DT_WEAK_VARIABLE DTAttributedTextContentView *weakself = self;
[_layoutFrame setTextBlockHandler:^(DTTextBlock *textBlock, CGRect frame, CGContextRef context, BOOL *shouldDrawDefaultBackground) {
DTAttributedTextContentView *strongself = weakself;
BOOL result = [strongself->_delegate attributedTextContentView:strongself shouldDrawBackgroundForTextBlock:textBlock frame:frame context:context forLayoutFrame:strongself->_layoutFrame];
if (shouldDrawDefaultBackground)
{
*shouldDrawDefaultBackground = result;
}
}];
}
[self invalidateAccessibilityElements];
}
}
return _layoutFrame;
}
}
- (void)setLayoutFrame:(DTCoreTextLayoutFrame *)layoutFrame
{
@synchronized(self)
{
if (_layoutFrame != layoutFrame)
{
[self removeAllCustomViewsForLinks];
if (layoutFrame)
{
[self setNeedsLayout];
[self setNeedsDisplayInRect:self.bounds];
}
_layoutFrame = layoutFrame;
[self invalidateAccessibilityElements];
}
};
}
- (NSMutableSet *)customViews
{
if (!customViews)
{
customViews = [[NSMutableSet alloc] init];
}
return customViews;
}
- (NSMutableDictionary *)customViewsForLinksIndex
{
if (!customViewsForLinksIndex)
{
customViewsForLinksIndex = [[NSMutableDictionary alloc] init];
}
return customViewsForLinksIndex;
}
- (NSMutableDictionary *)customViewsForAttachmentsIndex
{
if (!customViewsForAttachmentsIndex)
{
customViewsForAttachmentsIndex = [[NSMutableDictionary alloc] init];
}
return customViewsForAttachmentsIndex;
}
- (void)setDelegate:(id<DTAttributedTextContentViewDelegate>)delegate
{
_delegate = delegate;
_delegateFlags.delegateSupportsCustomViewsForAttachments = [_delegate respondsToSelector:@selector(attributedTextContentView:viewForAttachment:frame:)];
_delegateFlags.delegateSupportsCustomViewsForLinks = [_delegate respondsToSelector:@selector(attributedTextContentView:viewForLink:identifier:frame:)];
_delegateFlags.delegateSupportsGenericCustomViews = [_delegate respondsToSelector:@selector(attributedTextContentView:viewForAttributedString:frame:)];
_delegateFlags.delegateSupportsNotificationBeforeDrawing = [_delegate respondsToSelector:@selector(attributedTextContentView:willDrawLayoutFrame:inContext:)];
_delegateFlags.delegateSupportsNotificationAfterDrawing = [_delegate respondsToSelector:@selector(attributedTextContentView:didDrawLayoutFrame:inContext:)];
_delegateFlags.delegateSupportsNotificationBeforeTextBoxDrawing = [_delegate respondsToSelector:@selector(attributedTextContentView:shouldDrawBackgroundForTextBlock:frame:context:forLayoutFrame:)];
if (!_delegateFlags.delegateSupportsCustomViewsForLinks && !_delegateFlags.delegateSupportsGenericCustomViews)
{
[self removeAllCustomViewsForLinks];
}
// we don't draw the images if imageViews are provided by the delegate method
// if you want images to be drawn even though you use custom views, set it back to YES after setting delegate
if (_delegateFlags.delegateSupportsGenericCustomViews || _delegateFlags.delegateSupportsCustomViewsForAttachments)
{
_shouldDrawImages = NO;
}
else
{
_shouldDrawImages = YES;
}
}
#pragma mark - Accessibility
- (void)invalidateAccessibilityElements
{
_accessibilityElements = nil;
}
- (BOOL)isAccessibilityElement
{
return NO;
}
- (NSInteger)accessibilityElementCount
{
return [[self accessibilityElements] count];
}
- (id)accessibilityElementAtIndex:(NSInteger)index
{
DTAccessibilityElement *element = [[self accessibilityElements] objectAtIndex:index];
return element;
}
- (NSInteger)indexOfAccessibilityElement:(id)element
{
// It seems like indexOfObject: is failing for the proxy views, even though isEqual: and hash are both implemented
// on the proxy. Perhaps UIView doesn't like isEqual: with our proxy view. Regardless, this implementation seems to work.
NSInteger index = [[self accessibilityElements] indexOfObjectPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) {
BOOL equal = [obj isEqual:element];
*stop = equal;
return equal;
}];
return index;
}
- (NSArray *)accessibilityElements
{
if (!_accessibilityElements)
{
DTCoreTextLayoutFrameAccessibilityElementGenerator *generator = [[DTCoreTextLayoutFrameAccessibilityElementGenerator alloc] init];
_accessibilityElements = [generator accessibilityElementsForLayoutFrame:self.layoutFrame view:self attachmentViewProvider:^id(DTTextAttachment *attachment) {
// Since we actually take the views out of the view hierarchy when they're off screen, create a proxy object that stands
// in for the view until it's needed by VoiceOver. By the time VoiceOver asks for the view, it should already be onscreen.
return [[DTAccessibilityViewProxy alloc] initWithTextAttachment:attachment delegate:self];
}];
}
return _accessibilityElements;
}
#pragma mark - DTAccessibilityViewProxyDelegate
- (UIView *)viewForTextAttachment:(DTTextAttachment *)textAttachment proxy:(DTAccessibilityViewProxy *)proxy
{
NSNumber *indexKey = [NSNumber numberWithInteger:[textAttachment hash]];
UIView *existingAttachmentView = [self.customViewsForAttachmentsIndex objectForKey:indexKey];
return existingAttachmentView;
}
@synthesize layouter = _layouter;
@synthesize layoutFrame = _layoutFrame;
@synthesize attributedString = _attributedString;
@synthesize delegate = _delegate;
@synthesize edgeInsets = _edgeInsets;
@synthesize shouldDrawImages = _shouldDrawImages;
@synthesize shouldDrawLinks = _shouldDrawLinks;
@synthesize shouldLayoutCustomSubviews = _shouldLayoutCustomSubviews;
@synthesize layoutOffset = _layoutOffset;
@synthesize backgroundOffset = _backgroundOffset;
@synthesize customViews;
@synthesize customViewsForLinksIndex;
@synthesize customViewsForAttachmentsIndex;
@synthesize relayoutMask = _relayoutMask;
@synthesize accessibilityElements = _accessibilityElements;
@end
@implementation DTAttributedTextContentView (Drawing)
- (UIImage *)contentImageWithBounds:(CGRect)bounds options:(DTCoreTextLayoutFrameDrawingOptions)options
{
UIGraphicsBeginImageContextWithOptions(bounds.size, NO, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
if(!context) {
return nil;
}
CGContextTranslateCTM(context, -bounds.origin.x, -bounds.origin.y);
[self.layoutFrame drawInContext:context options:options];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
@end