YYTextSelectionView.m 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. //
  2. // YYTextSelectionView.m
  3. // YYKit <https://github.com/ibireme/YYKit>
  4. //
  5. // Created by ibireme on 15/2/25.
  6. // Copyright (c) 2015 ibireme.
  7. //
  8. // This source code is licensed under the MIT-style license found in the
  9. // LICENSE file in the root directory of this source tree.
  10. //
  11. #import "YYTextSelectionView.h"
  12. #import "YYCGUtilities.h"
  13. #import "YYWeakProxy.h"
  14. #define kMarkAlpha 0.2
  15. #define kLineWidth 2.0
  16. #define kBlinkDuration 0.5
  17. #define kBlinkFadeDuration 0.2
  18. #define kBlinkFirstDelay 0.1
  19. #define kTouchTestExtend 14.0
  20. #define kTouchDotExtend 7.0
  21. @implementation YYSelectionGrabberDot
  22. - (instancetype)initWithFrame:(CGRect)frame {
  23. self = [super initWithFrame:frame];
  24. if (!self) return nil;
  25. self.userInteractionEnabled = NO;
  26. self.mirror = [UIView new];
  27. return self;
  28. }
  29. - (void)layoutSubviews {
  30. [super layoutSubviews];
  31. CGFloat length = MIN(self.bounds.size.width, self.bounds.size.height);
  32. self.layer.cornerRadius = length * 0.5;
  33. self.mirror.bounds = self.bounds;
  34. self.mirror.layer.cornerRadius = self.layer.cornerRadius;
  35. }
  36. - (void)setBackgroundColor:(UIColor *)backgroundColor {
  37. [super setBackgroundColor:backgroundColor];
  38. _mirror.backgroundColor = backgroundColor;
  39. }
  40. @end
  41. @implementation YYSelectionGrabber
  42. - (instancetype) initWithFrame:(CGRect)frame {
  43. self = [super initWithFrame:frame];
  44. if (!self) return nil;
  45. _dot = [[YYSelectionGrabberDot alloc] initWithFrame:CGRectMake(0, 0, 10, 10)];
  46. return self;
  47. }
  48. - (void)setDotDirection:(YYTextDirection)dotDirection {
  49. _dotDirection = dotDirection;
  50. [self addSubview:_dot];
  51. CGRect frame = _dot.frame;
  52. CGFloat ofs = 0.5;
  53. if (dotDirection == YYTextDirectionTop) {
  54. frame.origin.y = -frame.size.height + ofs;
  55. frame.origin.x = (self.bounds.size.width - frame.size.width) / 2;
  56. } else if (dotDirection == YYTextDirectionRight) {
  57. frame.origin.x = self.bounds.size.width - ofs;
  58. frame.origin.y = (self.bounds.size.height - frame.size.height) / 2;
  59. } else if (dotDirection == YYTextDirectionBottom) {
  60. frame.origin.y = self.bounds.size.height - ofs;
  61. frame.origin.x = (self.bounds.size.width - frame.size.width) / 2;
  62. } else if (dotDirection == YYTextDirectionLeft) {
  63. frame.origin.x = -frame.size.width + ofs;
  64. frame.origin.y = (self.bounds.size.height - frame.size.height) / 2;
  65. } else {
  66. [_dot removeFromSuperview];
  67. }
  68. _dot.frame = frame;
  69. }
  70. - (void)setColor:(UIColor *)color {
  71. self.backgroundColor = color;
  72. _dot.backgroundColor = color;
  73. _color = color;
  74. }
  75. - (void)layoutSubviews {
  76. [super layoutSubviews];
  77. [self setDotDirection:_dotDirection];
  78. }
  79. - (CGRect)touchRect {
  80. CGRect rect = CGRectInset(self.frame, -kTouchTestExtend, -kTouchTestExtend);
  81. UIEdgeInsets insets = {0};
  82. if (_dotDirection == YYTextDirectionTop) {
  83. insets.top = -kTouchDotExtend;
  84. } else if (_dotDirection == YYTextDirectionRight) {
  85. insets.right = -kTouchDotExtend;
  86. } else if (_dotDirection == YYTextDirectionBottom) {
  87. insets.bottom = -kTouchDotExtend;
  88. } else if (_dotDirection == YYTextDirectionLeft) {
  89. insets.left = -kTouchDotExtend;
  90. }
  91. rect = UIEdgeInsetsInsetRect(rect, insets);
  92. return rect;
  93. }
  94. @end
  95. @interface YYTextSelectionView ()
  96. @property (nonatomic, strong) NSTimer *caretTimer;
  97. @property (nonatomic, strong) UIView *caretView;
  98. @property (nonatomic, strong) YYSelectionGrabber *startGrabber;
  99. @property (nonatomic, strong) YYSelectionGrabber *endGrabber;
  100. @property (nonatomic, strong) NSMutableArray *markViews;
  101. @end
  102. @implementation YYTextSelectionView
  103. - (instancetype)initWithFrame:(CGRect)frame {
  104. self = [super initWithFrame:frame];
  105. if (!self) return nil;
  106. self.userInteractionEnabled = NO;
  107. self.clipsToBounds = NO;
  108. _markViews = [NSMutableArray array];
  109. _caretView = [UIView new];
  110. _caretView.hidden = YES;
  111. _startGrabber = [YYSelectionGrabber new];
  112. _startGrabber.dotDirection = YYTextDirectionTop;
  113. _startGrabber.hidden = YES;
  114. _endGrabber = [YYSelectionGrabber new];
  115. _endGrabber.dotDirection = YYTextDirectionBottom;
  116. _endGrabber.hidden = YES;
  117. [self addSubview:_startGrabber];
  118. [self addSubview:_endGrabber];
  119. [self addSubview:_caretView];
  120. return self;
  121. }
  122. - (void)dealloc {
  123. [_caretTimer invalidate];
  124. }
  125. - (void)setColor:(UIColor *)color {
  126. _color = color;
  127. self.caretView.backgroundColor = color;
  128. self.startGrabber.color = color;
  129. self.endGrabber.color = color;
  130. [self.markViews enumerateObjectsUsingBlock: ^(UIView *v, NSUInteger idx, BOOL *stop) {
  131. v.backgroundColor = color;
  132. }];
  133. }
  134. - (void)setCaretBlinks:(BOOL)caretBlinks {
  135. if (_caretBlinks != caretBlinks) {
  136. _caretView.alpha = 1;
  137. [self.class cancelPreviousPerformRequestsWithTarget:self selector:@selector(_startBlinks) object:nil];
  138. if (caretBlinks) {
  139. [self performSelector:@selector(_startBlinks) withObject:nil afterDelay:kBlinkFirstDelay];
  140. } else {
  141. [_caretTimer invalidate];
  142. _caretTimer = nil;
  143. }
  144. _caretBlinks = caretBlinks;
  145. }
  146. }
  147. - (void)_startBlinks {
  148. [_caretTimer invalidate];
  149. if (_caretVisible) {
  150. _caretTimer = [NSTimer timerWithTimeInterval:kBlinkDuration target:[YYWeakProxy proxyWithTarget:self] selector:@selector(_doBlink) userInfo:nil repeats:YES];
  151. [[NSRunLoop currentRunLoop] addTimer:_caretTimer forMode:NSDefaultRunLoopMode];
  152. } else {
  153. _caretView.alpha = 1;
  154. }
  155. }
  156. - (void)_doBlink {
  157. [UIView animateWithDuration:kBlinkFadeDuration delay:0 options:UIViewAnimationOptionCurveEaseInOut animations: ^{
  158. if (_caretView.alpha == 1) _caretView.alpha = 0;
  159. else _caretView.alpha = 1;
  160. } completion:NULL];
  161. }
  162. - (void)setCaretVisible:(BOOL)caretVisible {
  163. _caretVisible = caretVisible;
  164. self.caretView.hidden = !caretVisible;
  165. _caretView.alpha = 1;
  166. [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_startBlinks) object:nil];
  167. if (_caretBlinks) {
  168. [self performSelector:@selector(_startBlinks) withObject:nil afterDelay:kBlinkFirstDelay];
  169. }
  170. }
  171. - (void)setVerticalForm:(BOOL)verticalForm {
  172. if (_verticalForm != verticalForm) {
  173. _verticalForm = verticalForm;
  174. [self setCaretRect:_caretRect];
  175. self.startGrabber.dotDirection = verticalForm ? YYTextDirectionRight : YYTextDirectionTop;
  176. self.endGrabber.dotDirection = verticalForm ? YYTextDirectionLeft : YYTextDirectionBottom;
  177. }
  178. }
  179. - (CGRect)_standardCaretRect:(CGRect)caretRect {
  180. caretRect = CGRectStandardize(caretRect);
  181. if (_verticalForm) {
  182. if (caretRect.size.height == 0) {
  183. caretRect.size.height = kLineWidth;
  184. caretRect.origin.y -= kLineWidth * 0.5;
  185. }
  186. if (caretRect.origin.y < 0) {
  187. caretRect.origin.y = 0;
  188. } else if (caretRect.origin.y + caretRect.size.height > self.bounds.size.height) {
  189. caretRect.origin.y = self.bounds.size.height - caretRect.size.height;
  190. }
  191. } else {
  192. if (caretRect.size.width == 0) {
  193. caretRect.size.width = kLineWidth;
  194. caretRect.origin.x -= kLineWidth * 0.5;
  195. }
  196. if (caretRect.origin.x < 0) {
  197. caretRect.origin.x = 0;
  198. } else if (caretRect.origin.x + caretRect.size.width > self.bounds.size.width) {
  199. caretRect.origin.x = self.bounds.size.width - caretRect.size.width;
  200. }
  201. }
  202. caretRect = CGRectPixelRound(caretRect);
  203. if (isnan(caretRect.origin.x) || isinf(caretRect.origin.x)) caretRect.origin.x = 0;
  204. if (isnan(caretRect.origin.y) || isinf(caretRect.origin.y)) caretRect.origin.y = 0;
  205. if (isnan(caretRect.size.width) || isinf(caretRect.size.width)) caretRect.size.width = 0;
  206. if (isnan(caretRect.size.height) || isinf(caretRect.size.height)) caretRect.size.height = 0;
  207. return caretRect;
  208. }
  209. - (void)setCaretRect:(CGRect)caretRect {
  210. _caretRect = caretRect;
  211. self.caretView.frame = [self _standardCaretRect:caretRect];
  212. CGFloat minWidth = MIN(self.caretView.bounds.size.width, self.caretView.bounds.size.height);
  213. self.caretView.layer.cornerRadius = minWidth / 2;
  214. }
  215. - (void)setSelectionRects:(NSArray *)selectionRects {
  216. _selectionRects = selectionRects.copy;
  217. [self.markViews enumerateObjectsUsingBlock: ^(UIView *v, NSUInteger idx, BOOL *stop) {
  218. [v removeFromSuperview];
  219. }];
  220. [self.markViews removeAllObjects];
  221. self.startGrabber.hidden = YES;
  222. self.endGrabber.hidden = YES;
  223. [selectionRects enumerateObjectsUsingBlock: ^(YYTextSelectionRect *r, NSUInteger idx, BOOL *stop) {
  224. CGRect rect = r.rect;
  225. rect = CGRectStandardize(rect);
  226. rect = CGRectPixelRound(rect);
  227. if (r.containsStart || r.containsEnd) {
  228. rect = [self _standardCaretRect:rect];
  229. if (r.containsStart) {
  230. self.startGrabber.hidden = NO;
  231. self.startGrabber.frame = rect;
  232. }
  233. if (r.containsEnd) {
  234. self.endGrabber.hidden = NO;
  235. self.endGrabber.frame = rect;
  236. }
  237. } else {
  238. if (rect.size.width > 0 && rect.size.height > 0) {
  239. UIView *mark = [[UIView alloc] initWithFrame:rect];
  240. mark.backgroundColor = _color;
  241. mark.alpha = kMarkAlpha;
  242. [self insertSubview:mark atIndex:0];
  243. [self.markViews addObject:mark];
  244. }
  245. }
  246. }];
  247. }
  248. - (BOOL)isGrabberContainsPoint:(CGPoint)point {
  249. return [self isStartGrabberContainsPoint:point] || [self isEndGrabberContainsPoint:point];
  250. }
  251. - (BOOL)isStartGrabberContainsPoint:(CGPoint)point {
  252. if (_startGrabber.hidden) return NO;
  253. CGRect startRect = [_startGrabber touchRect];
  254. CGRect endRect = [_endGrabber touchRect];
  255. if (CGRectIntersectsRect(startRect, endRect)) {
  256. CGFloat distStart = CGPointGetDistanceToPoint(point, CGRectGetCenter(startRect));
  257. CGFloat distEnd = CGPointGetDistanceToPoint(point, CGRectGetCenter(endRect));
  258. if (distEnd <= distStart) return NO;
  259. }
  260. return CGRectContainsPoint(startRect, point);
  261. }
  262. - (BOOL)isEndGrabberContainsPoint:(CGPoint)point {
  263. if (_endGrabber.hidden) return NO;
  264. CGRect startRect = [_startGrabber touchRect];
  265. CGRect endRect = [_endGrabber touchRect];
  266. if (CGRectIntersectsRect(startRect, endRect)) {
  267. CGFloat distStart = CGPointGetDistanceToPoint(point, CGRectGetCenter(startRect));
  268. CGFloat distEnd = CGPointGetDistanceToPoint(point, CGRectGetCenter(endRect));
  269. if (distEnd > distStart) return NO;
  270. }
  271. return CGRectContainsPoint(endRect, point);
  272. }
  273. - (BOOL)isCaretContainsPoint:(CGPoint)point {
  274. if (_caretVisible) {
  275. CGRect rect = CGRectInset(_caretRect, -kTouchTestExtend, -kTouchTestExtend);
  276. return CGRectContainsPoint(rect, point);
  277. }
  278. return NO;
  279. }
  280. - (BOOL)isSelectionRectsContainsPoint:(CGPoint)point {
  281. if (_selectionRects.count == 0) return NO;
  282. for (YYTextSelectionRect *rect in _selectionRects) {
  283. if (CGRectContainsPoint(rect.rect, point)) return YES;
  284. }
  285. return NO;
  286. }
  287. @end