// // YYTextSelectionView.m // YYKit // // 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 "YYCGUtilities.h" #import "YYWeakProxy.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:[YYWeakProxy 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 = CGRectPixelRound(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 = CGRectPixelRound(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 = CGPointGetDistanceToPoint(point, CGRectGetCenter(startRect)); CGFloat distEnd = CGPointGetDistanceToPoint(point, CGRectGetCenter(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 = CGPointGetDistanceToPoint(point, CGRectGetCenter(startRect)); CGFloat distEnd = CGPointGetDistanceToPoint(point, CGRectGetCenter(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