UNPKG

9.53 kBPlain TextView Raw
1/*
2 * Copyright (c) Facebook, Inc. and its affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8#import "RCTTVView.h"
9
10#import "RCTAutoInsetsProtocol.h"
11#import "RCTBorderDrawing.h"
12#import "RCTBridge.h"
13#import "RCTConvert.h"
14#import "RCTEventDispatcher.h"
15#import "RCTLog.h"
16#import "RCTRootViewInternal.h"
17#import "RCTTVNavigationEventEmitter.h"
18#import "RCTUtils.h"
19#import "RCTView.h"
20#import "UIView+React.h"
21
22@implementation RCTTVView
23{
24 UITapGestureRecognizer *_selectRecognizer;
25}
26
27- (instancetype)initWithFrame:(CGRect)frame
28{
29 if (self = [super initWithFrame:frame]) {
30 dispatch_once(&onceToken, ^{
31 defaultTVParallaxProperties = @{
32 @"enabled": @YES,
33 @"shiftDistanceX": @2.0f,
34 @"shiftDistanceY": @2.0f,
35 @"tiltAngle": @0.05f,
36 @"magnification": @1.0f,
37 @"pressMagnification": @1.0f,
38 @"pressDuration": @0.3f,
39 @"pressDelay": @0.0f
40 };
41 });
42 self.tvParallaxProperties = defaultTVParallaxProperties;
43 }
44
45 return self;
46}
47
48static NSDictionary* defaultTVParallaxProperties = nil;
49static dispatch_once_t onceToken;
50
51- (void)setTvParallaxProperties:(NSDictionary *)tvParallaxProperties {
52 if (_tvParallaxProperties == nil) {
53 _tvParallaxProperties = [defaultTVParallaxProperties copy];
54 return;
55 }
56
57 NSMutableDictionary *newParallaxProperties = [NSMutableDictionary dictionaryWithDictionary:_tvParallaxProperties];
58 for (NSString *k in [defaultTVParallaxProperties allKeys]) {
59 if (tvParallaxProperties[k]) {
60 newParallaxProperties[k] = tvParallaxProperties[k];
61 }
62 }
63 _tvParallaxProperties = [newParallaxProperties copy];
64}
65
66RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused)
67
68- (void)setIsTVSelectable:(BOOL)isTVSelectable {
69 self->_isTVSelectable = isTVSelectable;
70 if (isTVSelectable) {
71 UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc]
72 initWithTarget:self
73 action:@selector(handleSelect:)];
74 recognizer.allowedPressTypes = @[@(UIPressTypeSelect)];
75 _selectRecognizer = recognizer;
76 [self addGestureRecognizer:_selectRecognizer];
77 } else {
78 if(_selectRecognizer) {
79 [self removeGestureRecognizer:_selectRecognizer];
80 }
81 }
82}
83
84- (void)handleSelect:(__unused UIGestureRecognizer *)r
85{
86 if ([self.tvParallaxProperties[@"enabled"] boolValue] == YES) {
87 float magnification = [self.tvParallaxProperties[@"magnification"] floatValue];
88 float pressMagnification = [self.tvParallaxProperties[@"pressMagnification"] floatValue];
89
90 // Duration of press animation
91 float pressDuration = [self.tvParallaxProperties[@"pressDuration"] floatValue];
92
93 // Delay of press animation
94 float pressDelay = [self.tvParallaxProperties[@"pressDelay"] floatValue];
95
96 [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:pressDelay]];
97
98 [UIView animateWithDuration:(pressDuration/2)
99 animations:^{
100 self.transform = CGAffineTransformMakeScale(pressMagnification, pressMagnification);
101 }
102 completion:^(__unused BOOL finished1){
103 [UIView animateWithDuration:(pressDuration/2)
104 animations:^{
105 self.transform = CGAffineTransformMakeScale(magnification, magnification);
106 }
107 completion:^(__unused BOOL finished2) {
108 [[NSNotificationCenter defaultCenter] postNotificationName:RCTTVNavigationEventNotification
109 object:@{@"eventType":@"select",@"tag":self.reactTag}];
110 }];
111 }];
112
113 } else {
114 [[NSNotificationCenter defaultCenter] postNotificationName:RCTTVNavigationEventNotification
115 object:@{@"eventType":@"select",@"tag":self.reactTag}];
116 }
117}
118
119- (BOOL)isUserInteractionEnabled
120{
121 return YES;
122}
123
124- (BOOL)canBecomeFocused
125{
126 return (self.isTVSelectable);
127}
128
129- (void)addParallaxMotionEffects
130{
131 // Size of shift movements
132 CGFloat const shiftDistanceX = [self.tvParallaxProperties[@"shiftDistanceX"] floatValue];
133 CGFloat const shiftDistanceY = [self.tvParallaxProperties[@"shiftDistanceY"] floatValue];
134
135 // Make horizontal movements shift the centre left and right
136 UIInterpolatingMotionEffect *xShift = [[UIInterpolatingMotionEffect alloc]
137 initWithKeyPath:@"center.x"
138 type:UIInterpolatingMotionEffectTypeTiltAlongHorizontalAxis];
139 xShift.minimumRelativeValue = @( shiftDistanceX * -1.0f);
140 xShift.maximumRelativeValue = @( shiftDistanceX);
141
142 // Make vertical movements shift the centre up and down
143 UIInterpolatingMotionEffect *yShift = [[UIInterpolatingMotionEffect alloc]
144 initWithKeyPath:@"center.y"
145 type:UIInterpolatingMotionEffectTypeTiltAlongVerticalAxis];
146 yShift.minimumRelativeValue = @( shiftDistanceY * -1.0f);
147 yShift.maximumRelativeValue = @( shiftDistanceY);
148
149 // Size of tilt movements
150 CGFloat const tiltAngle = [self.tvParallaxProperties[@"tiltAngle"] floatValue];
151
152 // Now make horizontal movements effect a rotation about the Y axis for side-to-side rotation.
153 UIInterpolatingMotionEffect *xTilt = [[UIInterpolatingMotionEffect alloc]
154 initWithKeyPath:@"layer.transform"
155 type:UIInterpolatingMotionEffectTypeTiltAlongHorizontalAxis];
156
157 // CATransform3D value for minimumRelativeValue
158 CATransform3D transMinimumTiltAboutY = CATransform3DIdentity;
159 transMinimumTiltAboutY.m34 = 1.0 / 500;
160 transMinimumTiltAboutY = CATransform3DRotate(transMinimumTiltAboutY, tiltAngle * -1.0, 0, 1, 0);
161
162 // CATransform3D value for minimumRelativeValue
163 CATransform3D transMaximumTiltAboutY = CATransform3DIdentity;
164 transMaximumTiltAboutY.m34 = 1.0 / 500;
165 transMaximumTiltAboutY = CATransform3DRotate(transMaximumTiltAboutY, tiltAngle, 0, 1, 0);
166
167 // Set the transform property boundaries for the interpolation
168 xTilt.minimumRelativeValue = [NSValue valueWithCATransform3D: transMinimumTiltAboutY];
169 xTilt.maximumRelativeValue = [NSValue valueWithCATransform3D: transMaximumTiltAboutY];
170
171 // Now make vertical movements effect a rotation about the X axis for up and down rotation.
172 UIInterpolatingMotionEffect *yTilt = [[UIInterpolatingMotionEffect alloc]
173 initWithKeyPath:@"layer.transform"
174 type:UIInterpolatingMotionEffectTypeTiltAlongVerticalAxis];
175
176 // CATransform3D value for minimumRelativeValue
177 CATransform3D transMinimumTiltAboutX = CATransform3DIdentity;
178 transMinimumTiltAboutX.m34 = 1.0 / 500;
179 transMinimumTiltAboutX = CATransform3DRotate(transMinimumTiltAboutX, tiltAngle * -1.0, 1, 0, 0);
180
181 // CATransform3D value for minimumRelativeValue
182 CATransform3D transMaximumTiltAboutX = CATransform3DIdentity;
183 transMaximumTiltAboutX.m34 = 1.0 / 500;
184 transMaximumTiltAboutX = CATransform3DRotate(transMaximumTiltAboutX, tiltAngle, 1, 0, 0);
185
186 // Set the transform property boundaries for the interpolation
187 yTilt.minimumRelativeValue = [NSValue valueWithCATransform3D: transMinimumTiltAboutX];
188 yTilt.maximumRelativeValue = [NSValue valueWithCATransform3D: transMaximumTiltAboutX];
189
190 // Add all of the motion effects to this group
191 self.motionEffects = @[xShift, yShift, xTilt, yTilt];
192
193 float magnification = [self.tvParallaxProperties[@"magnification"] floatValue];
194
195 [UIView animateWithDuration:0.2 animations:^{
196 self.transform = CGAffineTransformMakeScale(magnification, magnification);
197 }];
198}
199
200- (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator
201{
202 if (context.nextFocusedView == self && self.isTVSelectable ) {
203 [self becomeFirstResponder];
204 [coordinator addCoordinatedAnimations:^(void){
205 if([self.tvParallaxProperties[@"enabled"] boolValue]) {
206 [self addParallaxMotionEffects];
207 }
208 [[NSNotificationCenter defaultCenter] postNotificationName:RCTTVNavigationEventNotification
209 object:@{@"eventType":@"focus",@"tag":self.reactTag}];
210 } completion:^(void){}];
211 } else {
212 [coordinator addCoordinatedAnimations:^(void){
213 [[NSNotificationCenter defaultCenter] postNotificationName:RCTTVNavigationEventNotification
214 object:@{@"eventType":@"blur",@"tag":self.reactTag}];
215 [UIView animateWithDuration:0.2 animations:^{
216 self.transform = CGAffineTransformMakeScale(1, 1);
217 }];
218
219 for (UIMotionEffect *effect in [self.motionEffects copy]){
220 [self removeMotionEffect:effect];
221 }
222 } completion:^(void){}];
223 [self resignFirstResponder];
224 }
225}
226
227- (void)setHasTVPreferredFocus:(BOOL)hasTVPreferredFocus
228{
229 _hasTVPreferredFocus = hasTVPreferredFocus;
230 if (hasTVPreferredFocus) {
231 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
232 UIView *rootview = self;
233 while (![rootview isReactRootView] && rootview != nil) {
234 rootview = [rootview superview];
235 }
236 if (rootview == nil) return;
237
238 rootview = [rootview superview];
239
240 [rootview setNeedsFocusUpdate];
241 [rootview updateFocusIfNeeded];
242 });
243 }
244}
245
246@end