New file |
| | |
| | | // |
| | | // YYTextSelectionView.m |
| | | // YYText <https://github.com/ibireme/YYText> |
| | | // |
| | | // Created by ibireme on 15/2/25. |
| | | // Copyright (c) 2015 ibireme. |
| | | // |
| | | // This source code is licensed under the MIT-style license found in the |
| | | // LICENSE file in the root directory of this source tree. |
| | | // |
| | | |
| | | #import "YYTextSelectionView.h" |
| | | #import "YYTextUtilities.h" |
| | | #import "YYTextWeakProxy.h" |
| | | |
| | | #define kMarkAlpha 0.2 |
| | | #define kLineWidth 2.0 |
| | | #define kBlinkDuration 0.5 |
| | | #define kBlinkFadeDuration 0.2 |
| | | #define kBlinkFirstDelay 0.1 |
| | | #define kTouchTestExtend 14.0 |
| | | #define kTouchDotExtend 7.0 |
| | | |
| | | |
| | | @implementation YYSelectionGrabberDot |
| | | |
| | | - (instancetype)initWithFrame:(CGRect)frame { |
| | | self = [super initWithFrame:frame]; |
| | | if (!self) return nil; |
| | | self.userInteractionEnabled = NO; |
| | | self.mirror = [UIView new]; |
| | | return self; |
| | | } |
| | | |
| | | - (void)layoutSubviews { |
| | | [super layoutSubviews]; |
| | | CGFloat length = MIN(self.bounds.size.width, self.bounds.size.height); |
| | | self.layer.cornerRadius = length * 0.5; |
| | | self.mirror.bounds = self.bounds; |
| | | self.mirror.layer.cornerRadius = self.layer.cornerRadius; |
| | | } |
| | | |
| | | - (void)setBackgroundColor:(UIColor *)backgroundColor { |
| | | [super setBackgroundColor:backgroundColor]; |
| | | _mirror.backgroundColor = backgroundColor; |
| | | } |
| | | |
| | | @end |
| | | |
| | | |
| | | |
| | | @implementation YYSelectionGrabber |
| | | |
| | | - (instancetype) initWithFrame:(CGRect)frame { |
| | | self = [super initWithFrame:frame]; |
| | | if (!self) return nil; |
| | | _dot = [[YYSelectionGrabberDot alloc] initWithFrame:CGRectMake(0, 0, 10, 10)]; |
| | | return self; |
| | | } |
| | | |
| | | - (void)setDotDirection:(YYTextDirection)dotDirection { |
| | | _dotDirection = dotDirection; |
| | | [self addSubview:_dot]; |
| | | CGRect frame = _dot.frame; |
| | | CGFloat ofs = 0.5; |
| | | if (dotDirection == YYTextDirectionTop) { |
| | | frame.origin.y = -frame.size.height + ofs; |
| | | frame.origin.x = (self.bounds.size.width - frame.size.width) / 2; |
| | | } else if (dotDirection == YYTextDirectionRight) { |
| | | frame.origin.x = self.bounds.size.width - ofs; |
| | | frame.origin.y = (self.bounds.size.height - frame.size.height) / 2; |
| | | } else if (dotDirection == YYTextDirectionBottom) { |
| | | frame.origin.y = self.bounds.size.height - ofs; |
| | | frame.origin.x = (self.bounds.size.width - frame.size.width) / 2; |
| | | } else if (dotDirection == YYTextDirectionLeft) { |
| | | frame.origin.x = -frame.size.width + ofs; |
| | | frame.origin.y = (self.bounds.size.height - frame.size.height) / 2; |
| | | } else { |
| | | [_dot removeFromSuperview]; |
| | | } |
| | | _dot.frame = frame; |
| | | } |
| | | |
| | | - (void)setColor:(UIColor *)color { |
| | | self.backgroundColor = color; |
| | | _dot.backgroundColor = color; |
| | | _color = color; |
| | | } |
| | | |
| | | - (void)layoutSubviews { |
| | | [super layoutSubviews]; |
| | | [self setDotDirection:_dotDirection]; |
| | | } |
| | | |
| | | - (CGRect)touchRect { |
| | | CGRect rect = CGRectInset(self.frame, -kTouchTestExtend, -kTouchTestExtend); |
| | | UIEdgeInsets insets = {0}; |
| | | if (_dotDirection == YYTextDirectionTop) { |
| | | insets.top = -kTouchDotExtend; |
| | | } else if (_dotDirection == YYTextDirectionRight) { |
| | | insets.right = -kTouchDotExtend; |
| | | } else if (_dotDirection == YYTextDirectionBottom) { |
| | | insets.bottom = -kTouchDotExtend; |
| | | } else if (_dotDirection == YYTextDirectionLeft) { |
| | | insets.left = -kTouchDotExtend; |
| | | } |
| | | rect = UIEdgeInsetsInsetRect(rect, insets); |
| | | return rect; |
| | | } |
| | | |
| | | @end |
| | | |
| | | |
| | | |
| | | @interface YYTextSelectionView () |
| | | @property (nonatomic, strong) NSTimer *caretTimer; |
| | | @property (nonatomic, strong) UIView *caretView; |
| | | @property (nonatomic, strong) YYSelectionGrabber *startGrabber; |
| | | @property (nonatomic, strong) YYSelectionGrabber *endGrabber; |
| | | @property (nonatomic, strong) NSMutableArray *markViews; |
| | | @end |
| | | |
| | | @implementation YYTextSelectionView |
| | | |
| | | - (instancetype)initWithFrame:(CGRect)frame { |
| | | self = [super initWithFrame:frame]; |
| | | if (!self) return nil; |
| | | |
| | | self.userInteractionEnabled = NO; |
| | | self.clipsToBounds = NO; |
| | | _markViews = [NSMutableArray array]; |
| | | _caretView = [UIView new]; |
| | | _caretView.hidden = YES; |
| | | _startGrabber = [YYSelectionGrabber new]; |
| | | _startGrabber.dotDirection = YYTextDirectionTop; |
| | | _startGrabber.hidden = YES; |
| | | _endGrabber = [YYSelectionGrabber new]; |
| | | _endGrabber.dotDirection = YYTextDirectionBottom; |
| | | _endGrabber.hidden = YES; |
| | | |
| | | [self addSubview:_startGrabber]; |
| | | [self addSubview:_endGrabber]; |
| | | [self addSubview:_caretView]; |
| | | |
| | | return self; |
| | | } |
| | | |
| | | - (void)dealloc { |
| | | [_caretTimer invalidate]; |
| | | } |
| | | |
| | | - (void)setColor:(UIColor *)color { |
| | | _color = color; |
| | | self.caretView.backgroundColor = color; |
| | | self.startGrabber.color = color; |
| | | self.endGrabber.color = color; |
| | | [self.markViews enumerateObjectsUsingBlock: ^(UIView *v, NSUInteger idx, BOOL *stop) { |
| | | v.backgroundColor = color; |
| | | }]; |
| | | } |
| | | |
| | | - (void)setCaretBlinks:(BOOL)caretBlinks { |
| | | if (_caretBlinks != caretBlinks) { |
| | | _caretView.alpha = 1; |
| | | [self.class cancelPreviousPerformRequestsWithTarget:self selector:@selector(_startBlinks) object:nil]; |
| | | if (caretBlinks) { |
| | | [self performSelector:@selector(_startBlinks) withObject:nil afterDelay:kBlinkFirstDelay]; |
| | | } else { |
| | | [_caretTimer invalidate]; |
| | | _caretTimer = nil; |
| | | } |
| | | _caretBlinks = caretBlinks; |
| | | } |
| | | } |
| | | |
| | | - (void)_startBlinks { |
| | | [_caretTimer invalidate]; |
| | | if (_caretVisible) { |
| | | _caretTimer = [NSTimer timerWithTimeInterval:kBlinkDuration target:[YYTextWeakProxy proxyWithTarget:self] selector:@selector(_doBlink) userInfo:nil repeats:YES]; |
| | | [[NSRunLoop currentRunLoop] addTimer:_caretTimer forMode:NSDefaultRunLoopMode]; |
| | | } else { |
| | | _caretView.alpha = 1; |
| | | } |
| | | } |
| | | |
| | | - (void)_doBlink { |
| | | [UIView animateWithDuration:kBlinkFadeDuration delay:0 options:UIViewAnimationOptionCurveEaseInOut animations: ^{ |
| | | if (_caretView.alpha == 1) _caretView.alpha = 0; |
| | | else _caretView.alpha = 1; |
| | | } completion:NULL]; |
| | | } |
| | | |
| | | - (void)setCaretVisible:(BOOL)caretVisible { |
| | | _caretVisible = caretVisible; |
| | | self.caretView.hidden = !caretVisible; |
| | | _caretView.alpha = 1; |
| | | [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_startBlinks) object:nil]; |
| | | if (_caretBlinks) { |
| | | [self performSelector:@selector(_startBlinks) withObject:nil afterDelay:kBlinkFirstDelay]; |
| | | } |
| | | } |
| | | |
| | | - (void)setVerticalForm:(BOOL)verticalForm { |
| | | if (_verticalForm != verticalForm) { |
| | | _verticalForm = verticalForm; |
| | | [self setCaretRect:_caretRect]; |
| | | self.startGrabber.dotDirection = verticalForm ? YYTextDirectionRight : YYTextDirectionTop; |
| | | self.endGrabber.dotDirection = verticalForm ? YYTextDirectionLeft : YYTextDirectionBottom; |
| | | } |
| | | } |
| | | |
| | | - (CGRect)_standardCaretRect:(CGRect)caretRect { |
| | | caretRect = CGRectStandardize(caretRect); |
| | | if (_verticalForm) { |
| | | if (caretRect.size.height == 0) { |
| | | caretRect.size.height = kLineWidth; |
| | | caretRect.origin.y -= kLineWidth * 0.5; |
| | | } |
| | | if (caretRect.origin.y < 0) { |
| | | caretRect.origin.y = 0; |
| | | } else if (caretRect.origin.y + caretRect.size.height > self.bounds.size.height) { |
| | | caretRect.origin.y = self.bounds.size.height - caretRect.size.height; |
| | | } |
| | | } else { |
| | | if (caretRect.size.width == 0) { |
| | | caretRect.size.width = kLineWidth; |
| | | caretRect.origin.x -= kLineWidth * 0.5; |
| | | } |
| | | if (caretRect.origin.x < 0) { |
| | | caretRect.origin.x = 0; |
| | | } else if (caretRect.origin.x + caretRect.size.width > self.bounds.size.width) { |
| | | caretRect.origin.x = self.bounds.size.width - caretRect.size.width; |
| | | } |
| | | } |
| | | caretRect = YYTextCGRectPixelRound(caretRect); |
| | | if (isnan(caretRect.origin.x) || isinf(caretRect.origin.x)) caretRect.origin.x = 0; |
| | | if (isnan(caretRect.origin.y) || isinf(caretRect.origin.y)) caretRect.origin.y = 0; |
| | | if (isnan(caretRect.size.width) || isinf(caretRect.size.width)) caretRect.size.width = 0; |
| | | if (isnan(caretRect.size.height) || isinf(caretRect.size.height)) caretRect.size.height = 0; |
| | | return caretRect; |
| | | } |
| | | |
| | | - (void)setCaretRect:(CGRect)caretRect { |
| | | _caretRect = caretRect; |
| | | self.caretView.frame = [self _standardCaretRect:caretRect]; |
| | | CGFloat minWidth = MIN(self.caretView.bounds.size.width, self.caretView.bounds.size.height); |
| | | self.caretView.layer.cornerRadius = minWidth / 2; |
| | | } |
| | | |
| | | - (void)setSelectionRects:(NSArray *)selectionRects { |
| | | _selectionRects = selectionRects.copy; |
| | | [self.markViews enumerateObjectsUsingBlock: ^(UIView *v, NSUInteger idx, BOOL *stop) { |
| | | [v removeFromSuperview]; |
| | | }]; |
| | | [self.markViews removeAllObjects]; |
| | | self.startGrabber.hidden = YES; |
| | | self.endGrabber.hidden = YES; |
| | | |
| | | [selectionRects enumerateObjectsUsingBlock: ^(YYTextSelectionRect *r, NSUInteger idx, BOOL *stop) { |
| | | CGRect rect = r.rect; |
| | | rect = CGRectStandardize(rect); |
| | | rect = YYTextCGRectPixelRound(rect); |
| | | if (r.containsStart || r.containsEnd) { |
| | | rect = [self _standardCaretRect:rect]; |
| | | if (r.containsStart) { |
| | | self.startGrabber.hidden = NO; |
| | | self.startGrabber.frame = rect; |
| | | } |
| | | if (r.containsEnd) { |
| | | self.endGrabber.hidden = NO; |
| | | self.endGrabber.frame = rect; |
| | | } |
| | | } else { |
| | | if (rect.size.width > 0 && rect.size.height > 0) { |
| | | UIView *mark = [[UIView alloc] initWithFrame:rect]; |
| | | mark.backgroundColor = _color; |
| | | mark.alpha = kMarkAlpha; |
| | | [self insertSubview:mark atIndex:0]; |
| | | [self.markViews addObject:mark]; |
| | | } |
| | | } |
| | | }]; |
| | | } |
| | | |
| | | - (BOOL)isGrabberContainsPoint:(CGPoint)point { |
| | | return [self isStartGrabberContainsPoint:point] || [self isEndGrabberContainsPoint:point]; |
| | | } |
| | | |
| | | - (BOOL)isStartGrabberContainsPoint:(CGPoint)point { |
| | | if (_startGrabber.hidden) return NO; |
| | | CGRect startRect = [_startGrabber touchRect]; |
| | | CGRect endRect = [_endGrabber touchRect]; |
| | | if (CGRectIntersectsRect(startRect, endRect)) { |
| | | CGFloat distStart = YYTextCGPointGetDistanceToPoint(point, YYTextCGRectGetCenter(startRect)); |
| | | CGFloat distEnd = YYTextCGPointGetDistanceToPoint(point, YYTextCGRectGetCenter(endRect)); |
| | | if (distEnd <= distStart) return NO; |
| | | } |
| | | return CGRectContainsPoint(startRect, point); |
| | | } |
| | | |
| | | - (BOOL)isEndGrabberContainsPoint:(CGPoint)point { |
| | | if (_endGrabber.hidden) return NO; |
| | | CGRect startRect = [_startGrabber touchRect]; |
| | | CGRect endRect = [_endGrabber touchRect]; |
| | | if (CGRectIntersectsRect(startRect, endRect)) { |
| | | CGFloat distStart = YYTextCGPointGetDistanceToPoint(point, YYTextCGRectGetCenter(startRect)); |
| | | CGFloat distEnd = YYTextCGPointGetDistanceToPoint(point, YYTextCGRectGetCenter(endRect)); |
| | | if (distEnd > distStart) return NO; |
| | | } |
| | | return CGRectContainsPoint(endRect, point); |
| | | } |
| | | |
| | | - (BOOL)isCaretContainsPoint:(CGPoint)point { |
| | | if (_caretVisible) { |
| | | CGRect rect = CGRectInset(_caretRect, -kTouchTestExtend, -kTouchTestExtend); |
| | | return CGRectContainsPoint(rect, point); |
| | | } |
| | | return NO; |
| | | } |
| | | |
| | | - (BOOL)isSelectionRectsContainsPoint:(CGPoint)point { |
| | | if (_selectionRects.count == 0) return NO; |
| | | for (YYTextSelectionRect *rect in _selectionRects) { |
| | | if (CGRectContainsPoint(rect.rect, point)) return YES; |
| | | } |
| | | return NO; |
| | | } |
| | | |
| | | @end |