New file |
| | |
| | | // |
| | | // YYTextEffectWindow.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 "YYTextEffectWindow.h" |
| | | #import "YYTextKeyboardManager.h" |
| | | #import "YYTextUtilities.h" |
| | | #import "UIView+YYText.h" |
| | | |
| | | |
| | | @implementation YYTextEffectWindow |
| | | |
| | | + (instancetype)sharedWindow { |
| | | static YYTextEffectWindow *one = nil; |
| | | if (one == nil) { |
| | | // iOS 9 compatible |
| | | NSString *mode = [NSRunLoop currentRunLoop].currentMode; |
| | | if (mode.length == 27 && |
| | | [mode hasPrefix:@"UI"] && |
| | | [mode hasSuffix:@"InitializationRunLoopMode"]) { |
| | | return nil; |
| | | } |
| | | } |
| | | |
| | | static dispatch_once_t onceToken; |
| | | dispatch_once(&onceToken, ^{ |
| | | if (!YYTextIsAppExtension()) { |
| | | one = [self new]; |
| | | one.frame = (CGRect){.size = YYTextScreenSize()}; |
| | | one.userInteractionEnabled = NO; |
| | | one.windowLevel = UIWindowLevelStatusBar + 1; |
| | | one.hidden = NO; |
| | | |
| | | // for iOS 9: |
| | | one.opaque = NO; |
| | | one.backgroundColor = [UIColor clearColor]; |
| | | one.layer.backgroundColor = [UIColor clearColor].CGColor; |
| | | } |
| | | }); |
| | | return one; |
| | | } |
| | | |
| | | // stop self from becoming the KeyWindow |
| | | - (void)becomeKeyWindow { |
| | | [[YYTextSharedApplication().delegate window] makeKeyWindow]; |
| | | } |
| | | |
| | | - (UIViewController *)rootViewController { |
| | | for (UIWindow *window in [YYTextSharedApplication() windows]) { |
| | | if (self == window) continue; |
| | | if (window.hidden) continue; |
| | | UIViewController *topViewController = window.rootViewController; |
| | | if (topViewController) return topViewController; |
| | | } |
| | | UIViewController *viewController = [super rootViewController]; |
| | | if (!viewController) { |
| | | viewController = [UIViewController new]; |
| | | [super setRootViewController:viewController]; |
| | | } |
| | | return viewController; |
| | | } |
| | | |
| | | // Bring self to front |
| | | - (void)_updateWindowLevel { |
| | | UIApplication *app = YYTextSharedApplication(); |
| | | if (!app) return; |
| | | |
| | | UIWindow *top = app.windows.lastObject; |
| | | UIWindow *key = app.keyWindow; |
| | | if (key && key.windowLevel > top.windowLevel) top = key; |
| | | if (top == self) return; |
| | | self.windowLevel = top.windowLevel + 1; |
| | | } |
| | | |
| | | - (YYTextDirection)_keyboardDirection { |
| | | CGRect keyboardFrame = [YYTextKeyboardManager defaultManager].keyboardFrame; |
| | | keyboardFrame = [[YYTextKeyboardManager defaultManager] convertRect:keyboardFrame toView:self]; |
| | | if (CGRectIsNull(keyboardFrame) || CGRectIsEmpty(keyboardFrame)) return YYTextDirectionNone; |
| | | |
| | | if (CGRectGetMinY(keyboardFrame) == 0 && |
| | | CGRectGetMinX(keyboardFrame) == 0 && |
| | | CGRectGetMaxX(keyboardFrame) == CGRectGetWidth(self.frame)) |
| | | return YYTextDirectionTop; |
| | | |
| | | if (CGRectGetMaxX(keyboardFrame) == CGRectGetWidth(self.frame) && |
| | | CGRectGetMinY(keyboardFrame) == 0 && |
| | | CGRectGetMaxY(keyboardFrame) == CGRectGetHeight(self.frame)) |
| | | return YYTextDirectionRight; |
| | | |
| | | if (CGRectGetMaxY(keyboardFrame) == CGRectGetHeight(self.frame) && |
| | | CGRectGetMinX(keyboardFrame) == 0 && |
| | | CGRectGetMaxX(keyboardFrame) == CGRectGetWidth(self.frame)) |
| | | return YYTextDirectionBottom; |
| | | |
| | | if (CGRectGetMinX(keyboardFrame) == 0 && |
| | | CGRectGetMinY(keyboardFrame) == 0 && |
| | | CGRectGetMaxY(keyboardFrame) == CGRectGetHeight(self.frame)) |
| | | return YYTextDirectionLeft; |
| | | |
| | | return YYTextDirectionNone; |
| | | } |
| | | |
| | | - (CGPoint)_correctedCaptureCenter:(CGPoint)center{ |
| | | CGRect keyboardFrame = [YYTextKeyboardManager defaultManager].keyboardFrame; |
| | | keyboardFrame = [[YYTextKeyboardManager defaultManager] convertRect:keyboardFrame toView:self]; |
| | | if (!CGRectIsNull(keyboardFrame) && !CGRectIsEmpty(keyboardFrame)) { |
| | | YYTextDirection direction = [self _keyboardDirection]; |
| | | switch (direction) { |
| | | case YYTextDirectionTop: { |
| | | if (center.y < CGRectGetMaxY(keyboardFrame)) center.y = CGRectGetMaxY(keyboardFrame); |
| | | } break; |
| | | case YYTextDirectionRight: { |
| | | if (center.x > CGRectGetMinX(keyboardFrame)) center.x = CGRectGetMinX(keyboardFrame); |
| | | } break; |
| | | case YYTextDirectionBottom: { |
| | | if (center.y > CGRectGetMinY(keyboardFrame)) center.y = CGRectGetMinY(keyboardFrame); |
| | | } break; |
| | | case YYTextDirectionLeft: { |
| | | if (center.x < CGRectGetMaxX(keyboardFrame)) center.x = CGRectGetMaxX(keyboardFrame); |
| | | } break; |
| | | default: break; |
| | | } |
| | | } |
| | | return center; |
| | | } |
| | | |
| | | - (CGPoint)_correctedCenter:(CGPoint)center forMagnifier:(YYTextMagnifier *)mag rotation:(CGFloat)rotation { |
| | | CGFloat degree = YYTextRadiansToDegrees(rotation); |
| | | |
| | | degree /= 45.0; |
| | | if (degree < 0) degree += (int)(-degree/8.0 + 1) * 8; |
| | | if (degree > 8) degree -= (int)(degree/8.0) * 8; |
| | | |
| | | CGFloat caretExt = 10; |
| | | if (degree <= 1 || degree >= 7) { //top |
| | | if (mag.type == YYTextMagnifierTypeCaret) { |
| | | if (center.y < caretExt) |
| | | center.y = caretExt; |
| | | } else if (mag.type == YYTextMagnifierTypeRanged) { |
| | | if (center.y < mag.bounds.size.height) |
| | | center.y = mag.bounds.size.height; |
| | | } |
| | | } else if (1 < degree && degree < 3) { // right |
| | | if (mag.type == YYTextMagnifierTypeCaret) { |
| | | if (center.x > self.bounds.size.width - caretExt) |
| | | center.x = self.bounds.size.width - caretExt; |
| | | } else if (mag.type == YYTextMagnifierTypeRanged) { |
| | | if (center.x > self.bounds.size.width - mag.bounds.size.height) |
| | | center.x = self.bounds.size.width - mag.bounds.size.height; |
| | | } |
| | | } else if (3 <= degree && degree <= 5) { // bottom |
| | | if (mag.type == YYTextMagnifierTypeCaret) { |
| | | if (center.y > self.bounds.size.height - caretExt) |
| | | center.y = self.bounds.size.height - caretExt; |
| | | } else if (mag.type == YYTextMagnifierTypeRanged) { |
| | | if (center.y > mag.bounds.size.height) |
| | | center.y = mag.bounds.size.height; |
| | | } |
| | | } else if (5 < degree && degree < 7) { // left |
| | | if (mag.type == YYTextMagnifierTypeCaret) { |
| | | if (center.x < caretExt) |
| | | center.x = caretExt; |
| | | } else if (mag.type == YYTextMagnifierTypeRanged) { |
| | | if (center.x < mag.bounds.size.height) |
| | | center.x = mag.bounds.size.height; |
| | | } |
| | | } |
| | | |
| | | |
| | | CGRect keyboardFrame = [YYTextKeyboardManager defaultManager].keyboardFrame; |
| | | keyboardFrame = [[YYTextKeyboardManager defaultManager] convertRect:keyboardFrame toView:self]; |
| | | if (!CGRectIsNull(keyboardFrame) && !CGRectIsEmpty(keyboardFrame)) { |
| | | YYTextDirection direction = [self _keyboardDirection]; |
| | | switch (direction) { |
| | | case YYTextDirectionTop: { |
| | | if (mag.type == YYTextMagnifierTypeCaret) { |
| | | if (center.y - mag.bounds.size.height / 2 < CGRectGetMaxY(keyboardFrame)) |
| | | center.y = CGRectGetMaxY(keyboardFrame) + mag.bounds.size.height / 2; |
| | | } else if (mag.type == YYTextMagnifierTypeRanged) { |
| | | if (center.y < CGRectGetMaxY(keyboardFrame)) center.y = CGRectGetMaxY(keyboardFrame); |
| | | } |
| | | } break; |
| | | case YYTextDirectionRight: { |
| | | if (mag.type == YYTextMagnifierTypeCaret) { |
| | | if (center.x + mag.bounds.size.height / 2 > CGRectGetMinX(keyboardFrame)) |
| | | center.x = CGRectGetMinX(keyboardFrame) - mag.bounds.size.width / 2; |
| | | } else if (mag.type == YYTextMagnifierTypeRanged) { |
| | | if (center.x > CGRectGetMinX(keyboardFrame)) center.x = CGRectGetMinX(keyboardFrame); |
| | | } |
| | | } break; |
| | | case YYTextDirectionBottom: { |
| | | if (mag.type == YYTextMagnifierTypeCaret) { |
| | | if (center.y + mag.bounds.size.height / 2 > CGRectGetMinY(keyboardFrame)) |
| | | center.y = CGRectGetMinY(keyboardFrame) - mag.bounds.size.height / 2; |
| | | } else if (mag.type == YYTextMagnifierTypeRanged) { |
| | | if (center.y > CGRectGetMinY(keyboardFrame)) center.y = CGRectGetMinY(keyboardFrame); |
| | | } |
| | | } break; |
| | | case YYTextDirectionLeft: { |
| | | if (mag.type == YYTextMagnifierTypeCaret) { |
| | | if (center.x - mag.bounds.size.height / 2 < CGRectGetMaxX(keyboardFrame)) |
| | | center.x = CGRectGetMaxX(keyboardFrame) + mag.bounds.size.width / 2; |
| | | } else if (mag.type == YYTextMagnifierTypeRanged) { |
| | | if (center.x < CGRectGetMaxX(keyboardFrame)) center.x = CGRectGetMaxX(keyboardFrame); |
| | | } |
| | | } break; |
| | | default: break; |
| | | } |
| | | } |
| | | |
| | | return center; |
| | | } |
| | | |
| | | /** |
| | | Capture screen snapshot and set it to magnifier. |
| | | @return Magnifier rotation radius. |
| | | */ |
| | | - (CGFloat)_updateMagnifier:(YYTextMagnifier *)mag { |
| | | UIApplication *app = YYTextSharedApplication(); |
| | | if (!app) return 0; |
| | | |
| | | UIView *hostView = mag.hostView; |
| | | UIWindow *hostWindow = [hostView isKindOfClass:[UIWindow class]] ? (id)hostView : hostView.window; |
| | | if (!hostView || !hostWindow) return 0; |
| | | CGPoint captureCenter = [self yy_convertPoint:mag.hostCaptureCenter fromViewOrWindow:hostView]; |
| | | captureCenter = [self _correctedCaptureCenter:captureCenter]; |
| | | CGRect captureRect = {.size = mag.snapshotSize}; |
| | | captureRect.origin.x = captureCenter.x - captureRect.size.width / 2; |
| | | captureRect.origin.y = captureCenter.y - captureRect.size.height / 2; |
| | | |
| | | CGAffineTransform trans = YYTextCGAffineTransformGetFromViews(hostView, self); |
| | | CGFloat rotation = YYTextCGAffineTransformGetRotation(trans); |
| | | |
| | | if (mag.captureDisabled) { |
| | | if (!mag.snapshot || mag.snapshot.size.width > 1) { |
| | | static UIImage *placeholder; |
| | | static dispatch_once_t onceToken; |
| | | dispatch_once(&onceToken, ^{ |
| | | CGRect rect = mag.bounds; |
| | | rect.origin = CGPointZero; |
| | | UIGraphicsBeginImageContextWithOptions(rect.size, NO, 0); |
| | | CGContextRef context = UIGraphicsGetCurrentContext(); |
| | | [[UIColor colorWithWhite:1 alpha:0.8] set]; |
| | | CGContextFillRect(context, rect); |
| | | placeholder = UIGraphicsGetImageFromCurrentImageContext(); |
| | | UIGraphicsEndImageContext(); |
| | | }); |
| | | mag.captureFadeAnimation = YES; |
| | | mag.snapshot = placeholder; |
| | | mag.captureFadeAnimation = NO; |
| | | } |
| | | return rotation; |
| | | } |
| | | |
| | | UIGraphicsBeginImageContextWithOptions(captureRect.size, NO, 0); |
| | | CGContextRef context = UIGraphicsGetCurrentContext(); |
| | | if (!context) return rotation; |
| | | |
| | | CGPoint tp = CGPointMake(captureRect.size.width / 2, captureRect.size.height / 2); |
| | | tp = CGPointApplyAffineTransform(tp, CGAffineTransformMakeRotation(rotation)); |
| | | CGContextRotateCTM(context, -rotation); |
| | | CGContextTranslateCTM(context, tp.x - captureCenter.x, tp.y - captureCenter.y); |
| | | |
| | | NSMutableArray *windows = app.windows.mutableCopy; |
| | | UIWindow *keyWindow = app.keyWindow; |
| | | if (![windows containsObject:keyWindow]) [windows addObject:keyWindow]; |
| | | [windows sortUsingComparator:^NSComparisonResult(UIWindow *w1, UIWindow *w2) { |
| | | if (w1.windowLevel < w2.windowLevel) return NSOrderedAscending; |
| | | else if (w1.windowLevel > w2.windowLevel) return NSOrderedDescending; |
| | | return NSOrderedSame; |
| | | }]; |
| | | UIScreen *mainScreen = [UIScreen mainScreen]; |
| | | for (UIWindow *window in windows) { |
| | | if (window.hidden || window.alpha <= 0.01) continue; |
| | | if (window.screen != mainScreen) continue; |
| | | if ([window isKindOfClass:self.class]) break; //don't capture window above self |
| | | CGContextSaveGState(context); |
| | | CGContextConcatCTM(context, YYTextCGAffineTransformGetFromViews(window, self)); |
| | | [window.layer renderInContext:context]; //render |
| | | //[window drawViewHierarchyInRect:window.bounds afterScreenUpdates:NO]; //slower when capture whole window |
| | | CGContextRestoreGState(context); |
| | | } |
| | | UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); |
| | | UIGraphicsEndImageContext(); |
| | | |
| | | if (mag.snapshot.size.width == 1) { |
| | | mag.captureFadeAnimation = YES; |
| | | } |
| | | mag.snapshot = image; |
| | | mag.captureFadeAnimation = NO; |
| | | return rotation; |
| | | } |
| | | |
| | | - (void)showMagnifier:(YYTextMagnifier *)mag { |
| | | if (!mag) return; |
| | | if (mag.superview != self) [self addSubview:mag]; |
| | | [self _updateWindowLevel]; |
| | | CGFloat rotation = [self _updateMagnifier:mag]; |
| | | CGPoint center = [self yy_convertPoint:mag.hostPopoverCenter fromViewOrWindow:mag.hostView]; |
| | | CGAffineTransform trans = CGAffineTransformMakeRotation(rotation); |
| | | trans = CGAffineTransformScale(trans, 0.3, 0.3); |
| | | mag.transform = trans; |
| | | mag.center = center; |
| | | if (mag.type == YYTextMagnifierTypeRanged) { |
| | | mag.alpha = 0; |
| | | } |
| | | NSTimeInterval time = mag.type == YYTextMagnifierTypeCaret ? 0.08 : 0.1; |
| | | [UIView animateWithDuration:time delay:0 options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionBeginFromCurrentState animations:^{ |
| | | if (mag.type == YYTextMagnifierTypeCaret) { |
| | | CGPoint newCenter = CGPointMake(0, -mag.fitSize.height / 2); |
| | | newCenter = CGPointApplyAffineTransform(newCenter, CGAffineTransformMakeRotation(rotation)); |
| | | newCenter.x += center.x; |
| | | newCenter.y += center.y; |
| | | mag.center = [self _correctedCenter:newCenter forMagnifier:mag rotation:rotation]; |
| | | } else { |
| | | mag.center = [self _correctedCenter:center forMagnifier:mag rotation:rotation]; |
| | | } |
| | | mag.transform = CGAffineTransformMakeRotation(rotation); |
| | | mag.alpha = 1; |
| | | } completion:^(BOOL finished) { |
| | | |
| | | }]; |
| | | } |
| | | |
| | | - (void)moveMagnifier:(YYTextMagnifier *)mag { |
| | | if (!mag) return; |
| | | [self _updateWindowLevel]; |
| | | CGFloat rotation = [self _updateMagnifier:mag]; |
| | | CGPoint center = [self yy_convertPoint:mag.hostPopoverCenter fromViewOrWindow:mag.hostView]; |
| | | if (mag.type == YYTextMagnifierTypeCaret) { |
| | | CGPoint newCenter = CGPointMake(0, -mag.fitSize.height / 2); |
| | | newCenter = CGPointApplyAffineTransform(newCenter, CGAffineTransformMakeRotation(rotation)); |
| | | newCenter.x += center.x; |
| | | newCenter.y += center.y; |
| | | mag.center = [self _correctedCenter:newCenter forMagnifier:mag rotation:rotation]; |
| | | } else { |
| | | mag.center = [self _correctedCenter:center forMagnifier:mag rotation:rotation]; |
| | | } |
| | | mag.transform = CGAffineTransformMakeRotation(rotation); |
| | | } |
| | | |
| | | - (void)hideMagnifier:(YYTextMagnifier *)mag { |
| | | if (!mag) return; |
| | | if (mag.superview != self) return; |
| | | CGFloat rotation = [self _updateMagnifier:mag]; |
| | | CGPoint center = [self yy_convertPoint:mag.hostPopoverCenter fromViewOrWindow:mag.hostView]; |
| | | NSTimeInterval time = mag.type == YYTextMagnifierTypeCaret ? 0.20 : 0.15; |
| | | [UIView animateWithDuration:time delay:0 options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionBeginFromCurrentState animations:^{ |
| | | |
| | | CGAffineTransform trans = CGAffineTransformMakeRotation(rotation); |
| | | trans = CGAffineTransformScale(trans, 0.01, 0.01); |
| | | mag.transform = trans; |
| | | |
| | | if (mag.type == YYTextMagnifierTypeCaret) { |
| | | CGPoint newCenter = CGPointMake(0, -mag.fitSize.height / 2); |
| | | newCenter = CGPointApplyAffineTransform(newCenter, CGAffineTransformMakeRotation(rotation)); |
| | | newCenter.x += center.x; |
| | | newCenter.y += center.y; |
| | | mag.center = [self _correctedCenter:newCenter forMagnifier:mag rotation:rotation]; |
| | | } else { |
| | | mag.center = [self _correctedCenter:center forMagnifier:mag rotation:rotation]; |
| | | mag.alpha = 0; |
| | | } |
| | | |
| | | } completion:^(BOOL finished) { |
| | | if (finished) { |
| | | [mag removeFromSuperview]; |
| | | mag.transform = CGAffineTransformIdentity; |
| | | mag.alpha = 1; |
| | | } |
| | | }]; |
| | | } |
| | | |
| | | - (void)_updateSelectionGrabberDot:(YYSelectionGrabberDot *)dot selection:(YYTextSelectionView *)selection{ |
| | | dot.mirror.hidden = YES; |
| | | if (selection.hostView.clipsToBounds == YES && dot.yy_visibleAlpha > 0.1) { |
| | | CGRect dotRect = [dot yy_convertRect:dot.bounds toViewOrWindow:self]; |
| | | BOOL dotInKeyboard = NO; |
| | | |
| | | CGRect keyboardFrame = [YYTextKeyboardManager defaultManager].keyboardFrame; |
| | | keyboardFrame = [[YYTextKeyboardManager defaultManager] convertRect:keyboardFrame toView:self]; |
| | | if (!CGRectIsNull(keyboardFrame) && !CGRectIsEmpty(keyboardFrame)) { |
| | | CGRect inter = CGRectIntersection(dotRect, keyboardFrame); |
| | | if (!CGRectIsNull(inter) && (inter.size.width > 1 || inter.size.height > 1)) { |
| | | dotInKeyboard = YES; |
| | | } |
| | | } |
| | | if (!dotInKeyboard) { |
| | | CGRect hostRect = [selection.hostView convertRect:selection.hostView.bounds toView:self]; |
| | | CGRect intersection = CGRectIntersection(dotRect, hostRect); |
| | | if (YYTextCGRectGetArea(intersection) < YYTextCGRectGetArea(dotRect)) { |
| | | CGFloat dist = YYTextCGPointGetDistanceToRect(YYTextCGRectGetCenter(dotRect), hostRect); |
| | | if (dist < CGRectGetWidth(dot.frame) * 0.55) { |
| | | dot.mirror.hidden = NO; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | CGPoint center = [dot yy_convertPoint:CGPointMake(CGRectGetWidth(dot.frame) / 2, CGRectGetHeight(dot.frame) / 2) toViewOrWindow:self]; |
| | | if (isnan(center.x) || isnan(center.y) || isinf(center.x) || isinf(center.y)) { |
| | | dot.mirror.hidden = YES; |
| | | } else { |
| | | dot.mirror.center = center; |
| | | } |
| | | } |
| | | |
| | | - (void)showSelectionDot:(YYTextSelectionView *)selection { |
| | | if (!selection) return; |
| | | [self _updateWindowLevel]; |
| | | [self insertSubview:selection.startGrabber.dot.mirror atIndex:0]; |
| | | [self insertSubview:selection.endGrabber.dot.mirror atIndex:0]; |
| | | [self _updateSelectionGrabberDot:selection.startGrabber.dot selection:selection]; |
| | | [self _updateSelectionGrabberDot:selection.endGrabber.dot selection:selection]; |
| | | } |
| | | |
| | | - (void)hideSelectionDot:(YYTextSelectionView *)selection { |
| | | if (!selection) return; |
| | | [selection.startGrabber.dot.mirror removeFromSuperview]; |
| | | [selection.endGrabber.dot.mirror removeFromSuperview]; |
| | | } |
| | | |
| | | @end |