This commit is contained in:
2014-08-08 13:27:54 +08:00
parent a7f10ddcbb
commit 698a479191
291 changed files with 53278 additions and 515 deletions
+413
View File
@@ -0,0 +1,413 @@
//
// DTAttributedTextView.m
// DTCoreText
//
// Created by Oliver Drobnik on 1/12/11.
// Copyright 2011 Drobnik.com. All rights reserved.
//
#import <QuartzCore/QuartzCore.h>
#import "DTAttributedTextView.h"
#import "DTCoreText.h"
#import "DTTiledLayerWithoutFade.h"
#import "DTBlockFunctions.h"
@interface DTAttributedTextView ()
- (void)_setup;
@end
@implementation DTAttributedTextView
{
UIView *_backgroundView;
// these are pass-through, i.e. store until the content view is created
DT_WEAK_VARIABLE id textDelegate;
NSAttributedString *_attributedString;
BOOL _shouldDrawLinks;
BOOL _shouldDrawImages;
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
[self _setup];
}
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)layoutSubviews
{
[super layoutSubviews];
[self attributedTextContentView];
// layout custom subviews for visible area
[_attributedTextContentView layoutSubviewsInRect:self.bounds];
}
- (void)awakeFromNib
{
[self _setup];
}
// default
- (void)_setup
{
if (self.backgroundColor)
{
CGFloat alpha = [self.backgroundColor alphaComponent];
if (alpha < 1.0)
{
self.opaque = NO;
}
else
{
self.opaque = YES;
}
}
else
{
self.backgroundColor = [DTColor whiteColor];
self.opaque = YES;
}
self.autoresizesSubviews = NO;
self.clipsToBounds = YES;
// defaults
_shouldDrawLinks = YES;
_shouldDrawImages = YES;
}
// override class e.g. for mutable content view
- (Class)classForContentView
{
return [DTAttributedTextContentView class];
}
#pragma mark External Methods
- (void)scrollToAnchorNamed:(NSString *)anchorName animated:(BOOL)animated
{
NSRange range = [self.attributedTextContentView.attributedString rangeOfAnchorNamed:anchorName];
if (range.location != NSNotFound)
{
[self scrollRangeToVisible:range animated:animated];
}
}
- (void)scrollRangeToVisible:(NSRange)range animated:(BOOL)animated
{
// get the line of the first index of the anchor range
DTCoreTextLayoutLine *line = [self.attributedTextContentView.layoutFrame lineContainingIndex:range.location];
// make sure we don't scroll too far
CGFloat maxScrollPos = self.contentSize.height - self.bounds.size.height + self.contentInset.bottom + self.contentInset.top;
CGFloat scrollPos = MIN(line.frame.origin.y, maxScrollPos);
// scroll
[self setContentOffset:CGPointMake(0, scrollPos) animated:animated];
}
- (void)relayoutText
{
DTBlockPerformSyncIfOnMainThreadElseAsync(^{
// need to reset the layouter because otherwise we get the old framesetter or cached layout frames
_attributedTextContentView.layouter=nil;
// here we're layouting the entire string, might be more efficient to only relayout the paragraphs that contain these attachments
[_attributedTextContentView relayoutText];
// layout custom subviews for visible area
[self setNeedsLayout];
});
}
#pragma mark - Working with a Cursor
- (NSInteger)closestCursorIndexToPoint:(CGPoint)point
{
// the point is in the coordinate system of the receiver, need to convert into those of the content view first
CGPoint pointInContentView = [self.attributedTextContentView convertPoint:point fromView:self];
return [self.attributedTextContentView closestCursorIndexToPoint:pointInContentView];
}
- (CGRect)cursorRectAtIndex:(NSInteger)index
{
CGRect rectInContentView = [self.attributedTextContentView cursorRectAtIndex:index];
// the point is in the coordinate system of the content view, need to convert into those of the receiver first
CGRect rect = [self.attributedTextContentView convertRect:rectInContentView toView:self];
return rect;
}
#pragma mark Notifications
- (void)contentViewDidLayout:(NSNotification *)notification
{
DTBlockPerformSyncIfOnMainThreadElseAsync(^{
NSDictionary *userInfo = [notification userInfo];
CGRect optimalFrame = [[userInfo objectForKey:@"OptimalFrame"] CGRectValue];
CGRect frame = UIEdgeInsetsInsetRect(self.bounds, self.contentInset);
// ignore possibly delayed layout notification for a different width
if (optimalFrame.size.width == frame.size.width)
{
_attributedTextContentView.frame = optimalFrame;
self.contentSize = [_attributedTextContentView intrinsicContentSize];
}
});
}
#pragma mark Properties
- (DTAttributedTextContentView *)attributedTextContentView
{
if (!_attributedTextContentView)
{
// subclasses can specify a DTAttributedTextContentView subclass instead
Class classToUse = [self classForContentView];
CGRect frame = UIEdgeInsetsInsetRect(self.bounds, self.contentInset);
if (frame.size.width<=0 || frame.size.height<=0)
{
frame = CGRectZero;
}
// make sure we always have a tiled layer
Class previousLayerClass = nil;
// for DTAttributedTextContentView subclasses we force a tiled layer
if ([classToUse isSubclassOfClass:[DTAttributedTextContentView class]])
{
Class layerClass = [DTAttributedTextContentView layerClass];
if (![layerClass isSubclassOfClass:[CATiledLayer class]])
{
[DTAttributedTextContentView setLayerClass:[DTTiledLayerWithoutFade class]];
previousLayerClass = layerClass;
}
}
_attributedTextContentView = [[classToUse alloc] initWithFrame:frame];
// restore previous layer class if we changed the layer class for the content view
if (previousLayerClass)
{
[DTAttributedTextContentView setLayerClass:previousLayerClass];
}
_attributedTextContentView.userInteractionEnabled = YES;
_attributedTextContentView.backgroundColor = self.backgroundColor;
_attributedTextContentView.shouldLayoutCustomSubviews = NO; // we call layout when scrolling
// adjust opaqueness based on background color alpha
CGFloat alpha = [self.backgroundColor alphaComponent];
if (alpha < 1.0)
{
_attributedTextContentView.opaque = NO;
}
else
{
_attributedTextContentView.opaque = YES;
}
// set text delegate if it was set before instantiation of content view
_attributedTextContentView.delegate = textDelegate;
// pass on setting
_attributedTextContentView.shouldDrawLinks = _shouldDrawLinks;
// notification that tells us about the actual size of the content view
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(contentViewDidLayout:) name:DTAttributedTextContentViewDidFinishLayoutNotification object:_attributedTextContentView];
// temporary frame to specify the width
_attributedTextContentView.frame = frame;
// set text we previously got, this also triggers a relayout
_attributedTextContentView.attributedString = _attributedString;
// this causes a relayout and the resulting notification will allow us to set the final frame
[self addSubview:_attributedTextContentView];
}
return _attributedTextContentView;
}
- (void)setBackgroundColor:(DTColor *)newColor
{
if ([newColor alphaComponent]<1.0)
{
super.backgroundColor = newColor;
_attributedTextContentView.backgroundColor = [DTColor clearColor];
self.opaque = NO;
}
else
{
super.backgroundColor = newColor;
if (_attributedTextContentView.opaque)
{
_attributedTextContentView.backgroundColor = newColor;
}
}
}
- (void)setContentInset:(UIEdgeInsets)contentInset
{
if (!UIEdgeInsetsEqualToEdgeInsets(self.contentInset, contentInset))
{
[super setContentInset:contentInset];
// height does not matter, that will be determined anyhow
CGRect contentFrame = CGRectMake(0, 0, self.frame.size.width - self.contentInset.left - self.contentInset.right, _attributedTextContentView.frame.size.height);
_attributedTextContentView.frame = contentFrame;
}
}
- (UIView *)backgroundView
{
if (!_backgroundView)
{
_backgroundView = [[UIView alloc] initWithFrame:self.bounds];
_backgroundView.backgroundColor = [DTColor whiteColor];
// default is no interaction because background should have no interaction
_backgroundView.userInteractionEnabled = NO;
[self insertSubview:_backgroundView belowSubview:self.attributedTextContentView];
// make content transparent so that we see the background
_attributedTextContentView.backgroundColor = [DTColor clearColor];
_attributedTextContentView.opaque = NO;
}
return _backgroundView;
}
- (void)setBackgroundView:(UIView *)backgroundView
{
if (_backgroundView != backgroundView)
{
[_backgroundView removeFromSuperview];
_backgroundView = backgroundView;
if (_attributedTextContentView)
{
[self insertSubview:_backgroundView belowSubview:_attributedTextContentView];
}
else
{
[self addSubview:_backgroundView];
}
if (_backgroundView)
{
// make content transparent so that we see the background
_attributedTextContentView.backgroundColor = [DTColor clearColor];
_attributedTextContentView.opaque = NO;
}
else
{
_attributedTextContentView.backgroundColor = [DTColor whiteColor];
_attributedTextContentView.opaque = YES;
}
}
}
- (void)setAttributedString:(NSAttributedString *)string
{
_attributedString = string;
// might need layout for visible custom views
[self setNeedsLayout];
if (_attributedTextContentView)
{
// pass it along if contentView already exists
_attributedTextContentView.attributedString = string;
// this causes a relayout and the resulting notification will allow us to set the frame and contentSize
}
}
- (NSAttributedString *)attributedString
{
return _attributedString;
}
- (void)setFrame:(CGRect)frame
{
CGRect oldFrame = self.frame;
if (!CGRectEqualToRect(oldFrame, frame))
{
[super setFrame:frame]; // need to set own frame first because layout completion needs this updated frame
if (oldFrame.size.width != frame.size.width)
{
// height does not matter, that will be determined anyhow
CGRect contentFrame = CGRectMake(0, 0, frame.size.width - self.contentInset.left - self.contentInset.right, _attributedTextContentView.frame.size.height);
_attributedTextContentView.frame = contentFrame;
}
}
}
- (void)setTextDelegate:(id<DTAttributedTextContentViewDelegate>)aTextDelegate
{
// store unsafe pointer to delegate because we might not have a contentView yet
textDelegate = aTextDelegate;
// set it if possible, otherwise it will be set in contentView lazy property
_attributedTextContentView.delegate = aTextDelegate;
}
- (id<DTAttributedTextContentViewDelegate>)textDelegate
{
return _attributedTextContentView.delegate;
}
- (void)setShouldDrawLinks:(BOOL)shouldDrawLinks
{
_shouldDrawLinks = shouldDrawLinks;
_attributedTextContentView.shouldDrawLinks = _shouldDrawLinks;
}
- (void)setShouldDrawImages:(BOOL)shouldDrawImages
{
_shouldDrawImages = shouldDrawImages;
_attributedTextContentView.shouldDrawImages = _shouldDrawImages;
}
@synthesize attributedTextContentView = _attributedTextContentView;
@synthesize attributedString = _attributedString;
@synthesize textDelegate = _textDelegate;
@synthesize shouldDrawLinks = _shouldDrawLinks;
@end