react-native/React/Modules/RCTKeyboardObserver.m

122 lines
3.5 KiB
Mathematica
Raw Normal View History

2015-08-25 10:30:51 +00:00
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
2015-08-25 10:30:51 +00:00
*/
#import "RCTKeyboardObserver.h"
#import "RCTEventDispatcher.h"
static NSDictionary *RCTParseKeyboardNotification(NSNotification *notification);
@implementation RCTKeyboardObserver
RCT_EXPORT_MODULE()
Added native event emitter Summary: This is a solution for the problem I raised in https://www.facebook.com/groups/react.native.community/permalink/768218933313687/ I've added a new native base class, `RCTEventEmitter` as well as an equivalent JS class/module `NativeEventEmitter` (RCTEventEmitter.js and EventEmitter.js were taken already). Instead of arbitrary modules sending events via `bridge.eventDispatcher`, the idea is that any module that sends events should now subclass `RCTEventEmitter`, and provide an equivalent JS module that subclasses `NativeEventEmitter`. JS code that wants to observe the events should now observe it via the specific JS module rather than via `RCTDeviceEventEmitter` directly. e.g. to observer a keyboard event, instead of writing: const RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); RCTDeviceEventEmitter.addListener('keyboardWillShow', (event) => { ... }); You'd now write: const Keyboard = require('Keyboard'); Keyboard.addListener('keyboardWillShow', (event) => { ... }); Within a component, you can also use the `Subscribable.Mixin` as you would previously, but instead of: this.addListenerOn(RCTDeviceEventEmitter, 'keyboardWillShow', ...); Write: this.addListenerOn(Keyboard, 'keyboardWillShow', ...); This approach allows the native `RCTKeyboardObserver` module to be created lazily the first time a listener is added, and to stop sending events when the last listener is removed. It also allows us to validate that the event strings being observed and omitted match the supported events for that module. As a proof-of-concept, I've converted the `RCTStatusBarManager` and `RCTKeyboardObserver` modules to use the new system. I'll convert the rest in a follow up diff. For now, the new `NativeEventEmitter` JS module wraps the `RCTDeviceEventEmitter` JS module, and just uses the native `RCTEventEmitter` module for bookkeeping. This allows for full backwards compatibility (code that is observing the event via `RCTDeviceEventEmitter` instead of the specific module will still work as expected, albeit with a warning). Once all legacy calls have been removed, this could be refactored to something more elegant internally, whilst maintaining the same public interface. Note: currently, all device events still share a single global namespace, since they're really all registered on the same emitter instance internally. We should move away from that as soon as possible because it's not intuitive and will likely lead to strange bugs if people add generic events such as "onChange" or "onError" to their modules (which is common practice for components, where it's not a problem). Reviewed By: javache Differential Revision: D3269966 fbshipit-source-id: 1412daba850cd373020e1086673ba38ef9193050
2016-05-11 13:26:53 +00:00
- (void)startObserving
2015-08-25 10:30:51 +00:00
{
#if !TARGET_OS_TV
Refactored module access to allow for lazy loading Summary: public The `bridge.modules` dictionary provides access to all native modules, but this API requires that every module is initialized in advance so that any module can be accessed. This diff introduces a better API that will allow modules to be initialized lazily as they are needed, and deprecates `bridge.modules` (modules that use it will still work, but should be rewritten to use `bridge.moduleClasses` or `-[bridge moduleForName/Class:` instead. The rules are now as follows: * Any module that overrides `init` or `setBridge:` will be initialized on the main thread when the bridge is created * Any module that implements `constantsToExport:` will be initialized later when the config is exported (the module itself will be initialized on a background queue, but `constantsToExport:` will still be called on the main thread. * All other modules will be initialized lazily when a method is first called on them. These rules may seem slightly arcane, but they have the advantage of not violating any assumptions that may have been made by existing code - any module written under the original assumption that it would be initialized synchronously on the main thread when the bridge is created should still function exactly the same, but modules that avoid overriding `init` or `setBridge:` will now be loaded lazily. I've rewritten most of the standard modules to take advantage of this new lazy loading, with the following results: Out of the 65 modules included in UIExplorer: * 16 are initialized on the main thread when the bridge is created * A further 8 are initialized when the config is exported to JS * The remaining 41 will be initialized lazily on-demand Reviewed By: jspahrsummers Differential Revision: D2677695 fb-gh-sync-id: 507ae7e9fd6b563e89292c7371767c978e928f33
2015-11-25 11:09:00 +00:00
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
2015-08-25 10:30:51 +00:00
#define ADD_KEYBOARD_HANDLER(NAME, SELECTOR) \
Refactored module access to allow for lazy loading Summary: public The `bridge.modules` dictionary provides access to all native modules, but this API requires that every module is initialized in advance so that any module can be accessed. This diff introduces a better API that will allow modules to be initialized lazily as they are needed, and deprecates `bridge.modules` (modules that use it will still work, but should be rewritten to use `bridge.moduleClasses` or `-[bridge moduleForName/Class:` instead. The rules are now as follows: * Any module that overrides `init` or `setBridge:` will be initialized on the main thread when the bridge is created * Any module that implements `constantsToExport:` will be initialized later when the config is exported (the module itself will be initialized on a background queue, but `constantsToExport:` will still be called on the main thread. * All other modules will be initialized lazily when a method is first called on them. These rules may seem slightly arcane, but they have the advantage of not violating any assumptions that may have been made by existing code - any module written under the original assumption that it would be initialized synchronously on the main thread when the bridge is created should still function exactly the same, but modules that avoid overriding `init` or `setBridge:` will now be loaded lazily. I've rewritten most of the standard modules to take advantage of this new lazy loading, with the following results: Out of the 65 modules included in UIExplorer: * 16 are initialized on the main thread when the bridge is created * A further 8 are initialized when the config is exported to JS * The remaining 41 will be initialized lazily on-demand Reviewed By: jspahrsummers Differential Revision: D2677695 fb-gh-sync-id: 507ae7e9fd6b563e89292c7371767c978e928f33
2015-11-25 11:09:00 +00:00
[nc addObserver:self selector:@selector(SELECTOR:) name:NAME object:nil]
2015-08-25 10:30:51 +00:00
Refactored module access to allow for lazy loading Summary: public The `bridge.modules` dictionary provides access to all native modules, but this API requires that every module is initialized in advance so that any module can be accessed. This diff introduces a better API that will allow modules to be initialized lazily as they are needed, and deprecates `bridge.modules` (modules that use it will still work, but should be rewritten to use `bridge.moduleClasses` or `-[bridge moduleForName/Class:` instead. The rules are now as follows: * Any module that overrides `init` or `setBridge:` will be initialized on the main thread when the bridge is created * Any module that implements `constantsToExport:` will be initialized later when the config is exported (the module itself will be initialized on a background queue, but `constantsToExport:` will still be called on the main thread. * All other modules will be initialized lazily when a method is first called on them. These rules may seem slightly arcane, but they have the advantage of not violating any assumptions that may have been made by existing code - any module written under the original assumption that it would be initialized synchronously on the main thread when the bridge is created should still function exactly the same, but modules that avoid overriding `init` or `setBridge:` will now be loaded lazily. I've rewritten most of the standard modules to take advantage of this new lazy loading, with the following results: Out of the 65 modules included in UIExplorer: * 16 are initialized on the main thread when the bridge is created * A further 8 are initialized when the config is exported to JS * The remaining 41 will be initialized lazily on-demand Reviewed By: jspahrsummers Differential Revision: D2677695 fb-gh-sync-id: 507ae7e9fd6b563e89292c7371767c978e928f33
2015-11-25 11:09:00 +00:00
ADD_KEYBOARD_HANDLER(UIKeyboardWillShowNotification, keyboardWillShow);
ADD_KEYBOARD_HANDLER(UIKeyboardDidShowNotification, keyboardDidShow);
ADD_KEYBOARD_HANDLER(UIKeyboardWillHideNotification, keyboardWillHide);
ADD_KEYBOARD_HANDLER(UIKeyboardDidHideNotification, keyboardDidHide);
ADD_KEYBOARD_HANDLER(UIKeyboardWillChangeFrameNotification, keyboardWillChangeFrame);
ADD_KEYBOARD_HANDLER(UIKeyboardDidChangeFrameNotification, keyboardDidChangeFrame);
2015-08-25 10:30:51 +00:00
#undef ADD_KEYBOARD_HANDLER
#endif
2015-08-25 10:30:51 +00:00
}
Added native event emitter Summary: This is a solution for the problem I raised in https://www.facebook.com/groups/react.native.community/permalink/768218933313687/ I've added a new native base class, `RCTEventEmitter` as well as an equivalent JS class/module `NativeEventEmitter` (RCTEventEmitter.js and EventEmitter.js were taken already). Instead of arbitrary modules sending events via `bridge.eventDispatcher`, the idea is that any module that sends events should now subclass `RCTEventEmitter`, and provide an equivalent JS module that subclasses `NativeEventEmitter`. JS code that wants to observe the events should now observe it via the specific JS module rather than via `RCTDeviceEventEmitter` directly. e.g. to observer a keyboard event, instead of writing: const RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); RCTDeviceEventEmitter.addListener('keyboardWillShow', (event) => { ... }); You'd now write: const Keyboard = require('Keyboard'); Keyboard.addListener('keyboardWillShow', (event) => { ... }); Within a component, you can also use the `Subscribable.Mixin` as you would previously, but instead of: this.addListenerOn(RCTDeviceEventEmitter, 'keyboardWillShow', ...); Write: this.addListenerOn(Keyboard, 'keyboardWillShow', ...); This approach allows the native `RCTKeyboardObserver` module to be created lazily the first time a listener is added, and to stop sending events when the last listener is removed. It also allows us to validate that the event strings being observed and omitted match the supported events for that module. As a proof-of-concept, I've converted the `RCTStatusBarManager` and `RCTKeyboardObserver` modules to use the new system. I'll convert the rest in a follow up diff. For now, the new `NativeEventEmitter` JS module wraps the `RCTDeviceEventEmitter` JS module, and just uses the native `RCTEventEmitter` module for bookkeeping. This allows for full backwards compatibility (code that is observing the event via `RCTDeviceEventEmitter` instead of the specific module will still work as expected, albeit with a warning). Once all legacy calls have been removed, this could be refactored to something more elegant internally, whilst maintaining the same public interface. Note: currently, all device events still share a single global namespace, since they're really all registered on the same emitter instance internally. We should move away from that as soon as possible because it's not intuitive and will likely lead to strange bugs if people add generic events such as "onChange" or "onError" to their modules (which is common practice for components, where it's not a problem). Reviewed By: javache Differential Revision: D3269966 fbshipit-source-id: 1412daba850cd373020e1086673ba38ef9193050
2016-05-11 13:26:53 +00:00
- (NSArray<NSString *> *)supportedEvents
{
return @[@"keyboardWillShow",
@"keyboardDidShow",
@"keyboardWillHide",
@"keyboardDidHide",
@"keyboardWillChangeFrame",
@"keyboardDidChangeFrame"];
}
- (void)stopObserving
2015-08-25 10:30:51 +00:00
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
// Bridge might be already invalidated by the time the keyboard is about to be dismissed.
// This might happen, for example, when reload from the packager is performed.
// Thus we need to check against nil here.
2015-08-25 10:30:51 +00:00
#define IMPLEMENT_KEYBOARD_HANDLER(EVENT) \
- (void)EVENT:(NSNotification *)notification \
{ \
if (!self.bridge) { \
return; \
} \
Added native event emitter Summary: This is a solution for the problem I raised in https://www.facebook.com/groups/react.native.community/permalink/768218933313687/ I've added a new native base class, `RCTEventEmitter` as well as an equivalent JS class/module `NativeEventEmitter` (RCTEventEmitter.js and EventEmitter.js were taken already). Instead of arbitrary modules sending events via `bridge.eventDispatcher`, the idea is that any module that sends events should now subclass `RCTEventEmitter`, and provide an equivalent JS module that subclasses `NativeEventEmitter`. JS code that wants to observe the events should now observe it via the specific JS module rather than via `RCTDeviceEventEmitter` directly. e.g. to observer a keyboard event, instead of writing: const RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); RCTDeviceEventEmitter.addListener('keyboardWillShow', (event) => { ... }); You'd now write: const Keyboard = require('Keyboard'); Keyboard.addListener('keyboardWillShow', (event) => { ... }); Within a component, you can also use the `Subscribable.Mixin` as you would previously, but instead of: this.addListenerOn(RCTDeviceEventEmitter, 'keyboardWillShow', ...); Write: this.addListenerOn(Keyboard, 'keyboardWillShow', ...); This approach allows the native `RCTKeyboardObserver` module to be created lazily the first time a listener is added, and to stop sending events when the last listener is removed. It also allows us to validate that the event strings being observed and omitted match the supported events for that module. As a proof-of-concept, I've converted the `RCTStatusBarManager` and `RCTKeyboardObserver` modules to use the new system. I'll convert the rest in a follow up diff. For now, the new `NativeEventEmitter` JS module wraps the `RCTDeviceEventEmitter` JS module, and just uses the native `RCTEventEmitter` module for bookkeeping. This allows for full backwards compatibility (code that is observing the event via `RCTDeviceEventEmitter` instead of the specific module will still work as expected, albeit with a warning). Once all legacy calls have been removed, this could be refactored to something more elegant internally, whilst maintaining the same public interface. Note: currently, all device events still share a single global namespace, since they're really all registered on the same emitter instance internally. We should move away from that as soon as possible because it's not intuitive and will likely lead to strange bugs if people add generic events such as "onChange" or "onError" to their modules (which is common practice for components, where it's not a problem). Reviewed By: javache Differential Revision: D3269966 fbshipit-source-id: 1412daba850cd373020e1086673ba38ef9193050
2016-05-11 13:26:53 +00:00
[self sendEventWithName:@#EVENT \
2015-08-25 10:30:51 +00:00
body:RCTParseKeyboardNotification(notification)]; \
}
IMPLEMENT_KEYBOARD_HANDLER(keyboardWillShow)
IMPLEMENT_KEYBOARD_HANDLER(keyboardDidShow)
IMPLEMENT_KEYBOARD_HANDLER(keyboardWillHide)
IMPLEMENT_KEYBOARD_HANDLER(keyboardDidHide)
IMPLEMENT_KEYBOARD_HANDLER(keyboardWillChangeFrame)
IMPLEMENT_KEYBOARD_HANDLER(keyboardDidChangeFrame)
@end
2015-09-02 16:09:16 +00:00
NS_INLINE NSDictionary *RCTRectDictionaryValue(CGRect rect)
2015-08-25 10:30:51 +00:00
{
return @{
@"screenX": @(rect.origin.x),
@"screenY": @(rect.origin.y),
@"width": @(rect.size.width),
@"height": @(rect.size.height),
};
}
2015-09-02 16:09:16 +00:00
static NSString *RCTAnimationNameForCurve(UIViewAnimationCurve curve)
{
switch (curve) {
case UIViewAnimationCurveEaseIn:
return @"easeIn";
case UIViewAnimationCurveEaseInOut:
return @"easeInEaseOut";
case UIViewAnimationCurveEaseOut:
return @"easeOut";
case UIViewAnimationCurveLinear:
return @"linear";
default:
return @"keyboard";
}
}
2015-08-25 10:30:51 +00:00
static NSDictionary *RCTParseKeyboardNotification(NSNotification *notification)
{
#if TARGET_OS_TV
return @{};
#else
2015-08-25 10:30:51 +00:00
NSDictionary *userInfo = notification.userInfo;
CGRect beginFrame = [userInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue];
CGRect endFrame = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
NSTimeInterval duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
2015-09-02 16:09:16 +00:00
UIViewAnimationCurve curve = [userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue];
2015-08-25 10:30:51 +00:00
return @{
@"startCoordinates": RCTRectDictionaryValue(beginFrame),
@"endCoordinates": RCTRectDictionaryValue(endFrame),
@"duration": @(duration * 1000.0), // ms
2015-09-02 16:09:16 +00:00
@"easing": RCTAnimationNameForCurve(curve),
2015-08-25 10:30:51 +00:00
};
#endif
2015-08-25 10:30:51 +00:00
}