/** * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ #import "RCTTVView.h" #import "RCTAutoInsetsProtocol.h" #import "RCTBorderDrawing.h" #import "RCTBridge.h" #import "RCTConvert.h" #import "RCTEventDispatcher.h" #import "RCTLog.h" #import "RCTRootViewInternal.h" #import "RCTTVNavigationEventEmitter.h" #import "RCTUtils.h" #import "RCTView.h" #import "UIView+React.h" @implementation RCTTVView { UITapGestureRecognizer *_selectRecognizer; } - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { self.tvParallaxProperties = @{ @"enabled": @YES, @"shiftDistanceX": @2.0f, @"shiftDistanceY": @2.0f, @"tiltAngle": @0.05f, @"magnification": @1.0f }; } return self; } RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused) - (void)setIsTVSelectable:(BOOL)isTVSelectable { self->_isTVSelectable = isTVSelectable; if(isTVSelectable) { UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleSelect:)]; recognizer.allowedPressTypes = @[@(UIPressTypeSelect)]; _selectRecognizer = recognizer; [self addGestureRecognizer:_selectRecognizer]; } else { if(_selectRecognizer) { [self removeGestureRecognizer:_selectRecognizer]; } } } - (void)handleSelect:(UIGestureRecognizer *)r { [[NSNotificationCenter defaultCenter] postNotificationName:RCTTVNavigationEventNotification object:@{@"eventType":@"select",@"tag":self.reactTag}]; } - (BOOL)isUserInteractionEnabled { return YES; } - (BOOL)canBecomeFocused { return (self.isTVSelectable); } - (void)addParallaxMotionEffects { // Size of shift movements CGFloat const shiftDistanceX = [self.tvParallaxProperties[@"shiftDistanceX"] floatValue]; CGFloat const shiftDistanceY = [self.tvParallaxProperties[@"shiftDistanceY"] floatValue]; // Make horizontal movements shift the centre left and right UIInterpolatingMotionEffect *xShift = [[UIInterpolatingMotionEffect alloc] initWithKeyPath:@"center.x" type:UIInterpolatingMotionEffectTypeTiltAlongHorizontalAxis]; xShift.minimumRelativeValue = @( shiftDistanceX * -1.0f); xShift.maximumRelativeValue = @( shiftDistanceX); // Make vertical movements shift the centre up and down UIInterpolatingMotionEffect *yShift = [[UIInterpolatingMotionEffect alloc] initWithKeyPath:@"center.y" type:UIInterpolatingMotionEffectTypeTiltAlongVerticalAxis]; yShift.minimumRelativeValue = @( shiftDistanceY * -1.0f); yShift.maximumRelativeValue = @( shiftDistanceY); // Size of tilt movements CGFloat const tiltAngle = [self.tvParallaxProperties[@"tiltAngle"] floatValue]; // Now make horizontal movements effect a rotation about the Y axis for side-to-side rotation. UIInterpolatingMotionEffect *xTilt = [[UIInterpolatingMotionEffect alloc] initWithKeyPath:@"layer.transform" type:UIInterpolatingMotionEffectTypeTiltAlongHorizontalAxis]; // CATransform3D value for minimumRelativeValue CATransform3D transMinimumTiltAboutY = CATransform3DIdentity; transMinimumTiltAboutY.m34 = 1.0 / 500; transMinimumTiltAboutY = CATransform3DRotate(transMinimumTiltAboutY, tiltAngle * -1.0, 0, 1, 0); // CATransform3D value for minimumRelativeValue CATransform3D transMaximumTiltAboutY = CATransform3DIdentity; transMaximumTiltAboutY.m34 = 1.0 / 500; transMaximumTiltAboutY = CATransform3DRotate(transMaximumTiltAboutY, tiltAngle, 0, 1, 0); // Set the transform property boundaries for the interpolation xTilt.minimumRelativeValue = [NSValue valueWithCATransform3D: transMinimumTiltAboutY]; xTilt.maximumRelativeValue = [NSValue valueWithCATransform3D: transMaximumTiltAboutY]; // Now make vertical movements effect a rotation about the X axis for up and down rotation. UIInterpolatingMotionEffect *yTilt = [[UIInterpolatingMotionEffect alloc] initWithKeyPath:@"layer.transform" type:UIInterpolatingMotionEffectTypeTiltAlongVerticalAxis]; // CATransform3D value for minimumRelativeValue CATransform3D transMinimumTiltAboutX = CATransform3DIdentity; transMinimumTiltAboutX.m34 = 1.0 / 500; transMinimumTiltAboutX = CATransform3DRotate(transMinimumTiltAboutX, tiltAngle * -1.0, 1, 0, 0); // CATransform3D value for minimumRelativeValue CATransform3D transMaximumTiltAboutX = CATransform3DIdentity; transMaximumTiltAboutX.m34 = 1.0 / 500; transMaximumTiltAboutX = CATransform3DRotate(transMaximumTiltAboutX, tiltAngle, 1, 0, 0); // Set the transform property boundaries for the interpolation yTilt.minimumRelativeValue = [NSValue valueWithCATransform3D: transMinimumTiltAboutX]; yTilt.maximumRelativeValue = [NSValue valueWithCATransform3D: transMaximumTiltAboutX]; // Add all of the motion effects to this group self.motionEffects = @[xShift, yShift, xTilt, yTilt]; float magnification = [self.tvParallaxProperties[@"magnification"] floatValue]; [UIView animateWithDuration:0.2 animations:^{ self.transform = CGAffineTransformMakeScale(magnification, magnification); }]; } - (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator { if (context.nextFocusedView == self && self.isTVSelectable ) { [self becomeFirstResponder]; [coordinator addCoordinatedAnimations:^(void){ if([self.tvParallaxProperties[@"enabled"] boolValue]) { [self addParallaxMotionEffects]; } [[NSNotificationCenter defaultCenter] postNotificationName:RCTTVNavigationEventNotification object:@{@"eventType":@"focus",@"tag":self.reactTag}]; } completion:^(void){}]; } else { [coordinator addCoordinatedAnimations:^(void){ [[NSNotificationCenter defaultCenter] postNotificationName:RCTTVNavigationEventNotification object:@{@"eventType":@"blur",@"tag":self.reactTag}]; [UIView animateWithDuration:0.2 animations:^{ self.transform = CGAffineTransformMakeScale(1, 1); }]; for (UIMotionEffect *effect in [self.motionEffects copy]){ [self removeMotionEffect:effect]; } } completion:^(void){}]; [self resignFirstResponder]; } } - (void)setHasTVPreferredFocus:(BOOL)hasTVPreferredFocus { _hasTVPreferredFocus = hasTVPreferredFocus; if (hasTVPreferredFocus) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ UIView *rootview = self; while (![rootview isReactRootView] && rootview != nil) { rootview = [rootview superview]; } if (rootview == nil) return; rootview = [rootview superview]; [(RCTRootView *)rootview setReactPreferredFocusedView:self]; [rootview setNeedsFocusUpdate]; [rootview updateFocusIfNeeded]; }); } } @end