Files
react-native-reanimated/Example/ios/MBFingerTipWindow.m
Krzysztof Magiera 3571bbdd1c Add finger tracing
2018-05-17 10:53:15 +02:00

402 lines
12 KiB
Objective-C

//
// MBFingerTipWindow.m
//
// Copyright 2011-2017 Mapbox, Inc. All rights reserved.
//
#import "MBFingerTipWindow.h"
// This file must be built with ARC.
//
#if !__has_feature(objc_arc)
#error "ARC must be enabled for MBFingerTipWindow.m"
#endif
@interface MBFingerTipView : UIImageView
@property (nonatomic, assign) NSTimeInterval timestamp;
@property (nonatomic, assign) BOOL shouldAutomaticallyRemoveAfterTimeout;
@property (nonatomic, assign, getter=isFadingOut) BOOL fadingOut;
@end
#pragma mark -
@interface MBFingerTipOverlayWindow : UIWindow
@end
#pragma mark -
@interface MBFingerTipWindow ()
@property (nonatomic, strong) UIWindow *overlayWindow;
@property (nonatomic, assign) BOOL active;
@property (nonatomic, assign) BOOL fingerTipRemovalScheduled;
- (void)MBFingerTipWindow_commonInit;
- (BOOL)anyScreenIsMirrored;
- (void)updateFingertipsAreActive;
- (void)scheduleFingerTipRemoval;
- (void)cancelScheduledFingerTipRemoval;
- (void)removeInactiveFingerTips;
- (void)removeFingerTipWithHash:(NSUInteger)hash animated:(BOOL)animated;
- (BOOL)shouldAutomaticallyRemoveFingerTipForTouch:(UITouch *)touch;
@end
#pragma mark -
@implementation MBFingerTipWindow
@synthesize touchImage=_touchImage;
- (id)initWithCoder:(NSCoder *)decoder
{
// This covers NIB-loaded windows.
//
self = [super initWithCoder:decoder];
if (self != nil)
[self MBFingerTipWindow_commonInit];
return self;
}
- (id)initWithFrame:(CGRect)rect
{
// This covers programmatically-created windows.
//
self = [super initWithFrame:rect];
if (self != nil)
[self MBFingerTipWindow_commonInit];
return self;
}
- (void)MBFingerTipWindow_commonInit
{
self.strokeColor = [UIColor blackColor];
self.fillColor = [UIColor whiteColor];
self.touchAlpha = 0.5;
self.fadeDuration = 0.3;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(screenConnect:)
name:UIScreenDidConnectNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(screenDisconnect:)
name:UIScreenDidDisconnectNotification
object:nil];
// Set up active now, in case the screen was present before the window was created (or application launched).
//
[self updateFingertipsAreActive];
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIScreenDidConnectNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIScreenDidDisconnectNotification object:nil];
}
#pragma mark -
- (UIWindow *)overlayWindow
{
if ( ! _overlayWindow)
{
_overlayWindow = [[MBFingerTipOverlayWindow alloc] initWithFrame:self.frame];
_overlayWindow.userInteractionEnabled = NO;
_overlayWindow.windowLevel = UIWindowLevelStatusBar;
_overlayWindow.backgroundColor = [UIColor clearColor];
_overlayWindow.hidden = NO;
}
return _overlayWindow;
}
- (UIImage *)touchImage
{
if ( ! _touchImage)
{
UIBezierPath *clipPath = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, 50.0, 50.0)];
UIGraphicsBeginImageContextWithOptions(clipPath.bounds.size, NO, 0);
UIBezierPath *drawPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(25.0, 25.0)
radius:22.0
startAngle:0
endAngle:2 * M_PI
clockwise:YES];
drawPath.lineWidth = 2.0;
[self.strokeColor setStroke];
[self.fillColor setFill];
[drawPath stroke];
[drawPath fill];
[clipPath addClip];
_touchImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
return _touchImage;
}
#pragma mark - Setter
- (void)setAlwaysShowTouches:(BOOL)flag
{
if (_alwaysShowTouches != flag)
{
_alwaysShowTouches = flag;
[self updateFingertipsAreActive];
}
}
#pragma mark -
#pragma mark Screen notifications
- (void)screenConnect:(NSNotification *)notification
{
[self updateFingertipsAreActive];
}
- (void)screenDisconnect:(NSNotification *)notification
{
[self updateFingertipsAreActive];
}
- (BOOL)anyScreenIsMirrored
{
if ( ! [UIScreen instancesRespondToSelector:@selector(mirroredScreen)])
return NO;
for (UIScreen *screen in [UIScreen screens])
{
if ([screen mirroredScreen] != nil)
return YES;
}
return NO;
}
- (void)updateFingertipsAreActive;
{
if (self.alwaysShowTouches || ([[[[NSProcessInfo processInfo] environment] objectForKey:@"DEBUG_FINGERTIP_WINDOW"] boolValue]))
{
self.active = YES;
}
else
{
self.active = [self anyScreenIsMirrored];
}
}
#pragma mark -
#pragma mark UIWindow overrides
- (void)sendEvent:(UIEvent *)event
{
if (self.active)
{
NSSet *allTouches = [event allTouches];
for (UITouch *touch in [allTouches allObjects])
{
switch (touch.phase)
{
case UITouchPhaseBegan:
case UITouchPhaseMoved:
case UITouchPhaseStationary:
{
MBFingerTipView *touchView = (MBFingerTipView *)[self.overlayWindow viewWithTag:touch.hash];
if (touch.phase != UITouchPhaseStationary && touchView != nil && [touchView isFadingOut])
{
[touchView removeFromSuperview];
touchView = nil;
}
if (touchView == nil && touch.phase != UITouchPhaseStationary)
{
touchView = [[MBFingerTipView alloc] initWithImage:self.touchImage];
[self.overlayWindow addSubview:touchView];
}
if ( ! [touchView isFadingOut])
{
touchView.alpha = self.touchAlpha;
touchView.center = [touch locationInView:self.overlayWindow];
touchView.tag = touch.hash;
touchView.timestamp = touch.timestamp;
touchView.shouldAutomaticallyRemoveAfterTimeout = [self shouldAutomaticallyRemoveFingerTipForTouch:touch];
}
break;
}
case UITouchPhaseEnded:
case UITouchPhaseCancelled:
{
[self removeFingerTipWithHash:touch.hash animated:YES];
break;
}
}
}
}
[super sendEvent:event];
[self scheduleFingerTipRemoval]; // We may not see all UITouchPhaseEnded/UITouchPhaseCancelled events.
}
#pragma mark -
#pragma mark Private
- (void)scheduleFingerTipRemoval
{
if (self.fingerTipRemovalScheduled)
return;
self.fingerTipRemovalScheduled = YES;
[self performSelector:@selector(removeInactiveFingerTips) withObject:nil afterDelay:0.1];
}
- (void)cancelScheduledFingerTipRemoval
{
self.fingerTipRemovalScheduled = YES;
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(removeInactiveFingerTips) object:nil];
}
- (void)removeInactiveFingerTips
{
self.fingerTipRemovalScheduled = NO;
NSTimeInterval now = [[NSProcessInfo processInfo] systemUptime];
const CGFloat REMOVAL_DELAY = 0.2;
for (MBFingerTipView *touchView in [self.overlayWindow subviews])
{
if ( ! [touchView isKindOfClass:[MBFingerTipView class]])
continue;
if (touchView.shouldAutomaticallyRemoveAfterTimeout && now > touchView.timestamp + REMOVAL_DELAY)
[self removeFingerTipWithHash:touchView.tag animated:YES];
}
if ([[self.overlayWindow subviews] count] > 0)
[self scheduleFingerTipRemoval];
}
- (void)removeFingerTipWithHash:(NSUInteger)hash animated:(BOOL)animated;
{
MBFingerTipView *touchView = (MBFingerTipView *)[self.overlayWindow viewWithTag:hash];
if ( ! [touchView isKindOfClass:[MBFingerTipView class]])
return;
if ([touchView isFadingOut])
return;
BOOL animationsWereEnabled = [UIView areAnimationsEnabled];
if (animated)
{
[UIView setAnimationsEnabled:YES];
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:self.fadeDuration];
}
touchView.frame = CGRectMake(touchView.center.x - touchView.frame.size.width,
touchView.center.y - touchView.frame.size.height,
touchView.frame.size.width * 2,
touchView.frame.size.height * 2);
touchView.alpha = 0.0;
if (animated)
{
[UIView commitAnimations];
[UIView setAnimationsEnabled:animationsWereEnabled];
}
touchView.fadingOut = YES;
[touchView performSelector:@selector(removeFromSuperview) withObject:nil afterDelay:self.fadeDuration];
}
- (BOOL)shouldAutomaticallyRemoveFingerTipForTouch:(UITouch *)touch;
{
// We don't reliably get UITouchPhaseEnded or UITouchPhaseCancelled
// events via -sendEvent: for certain touch events. Known cases
// include swipe-to-delete on a table view row, and tap-to-cancel
// swipe to delete. We automatically remove their associated
// fingertips after a suitable timeout.
//
// It would be much nicer if we could remove all touch events after
// a suitable time out, but then we'll prematurely remove touch and
// hold events that are picked up by gesture recognizers (since we
// don't use UITouchPhaseStationary touches for those. *sigh*). So we
// end up with this more complicated setup.
UIView *view = [touch view];
view = [view hitTest:[touch locationInView:view] withEvent:nil];
while (view != nil)
{
if ([view isKindOfClass:[UITableViewCell class]])
{
for (UIGestureRecognizer *recognizer in [touch gestureRecognizers])
{
if ([recognizer isKindOfClass:[UISwipeGestureRecognizer class]])
return YES;
}
}
if ([view isKindOfClass:[UITableView class]])
{
if ([[touch gestureRecognizers] count] == 0)
return YES;
}
view = view.superview;
}
return NO;
}
@end
#pragma mark -
@implementation MBFingerTipView
@end
#pragma mark -
@implementation MBFingerTipOverlayWindow
// UIKit tries to get the rootViewController from the overlay window. Use the Fingertips window instead. This fixes
// issues with status bar behavior, as otherwise the overlay window would control the status bar.
- (UIViewController *)rootViewController
{
NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings)
{
return [evaluatedObject isKindOfClass:[MBFingerTipWindow class]];
}];
UIWindow *mainWindow = [[[[UIApplication sharedApplication] windows] filteredArrayUsingPredicate:predicate] firstObject];
return mainWindow.rootViewController ?: [super rootViewController];
}
@end