1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
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 |
|
48 | static NSDictionary* defaultTVParallaxProperties = nil;
|
49 | static 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 |
|
66 | RCT_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
|