mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-05-14 16:39:47 +08:00
Summary: This PR adds `isReduceMotionEnabled()` to `AccessibilityInfo` in other to add support for "reduce motion", exposing the Operational System's settings option. Additionally, it adds a new event, `reduceMotionChanged`, in order to listen for this flag's update. With this feature, developers will be able to disable or reduce animations, _**something that will be required as soon as WCAG 2.1 draft got approved**._ See [WCAG 2.1 — 2.3.3 Animations from Interaction criteria](https://knowbility.org/blog/2018/WCAG21-233Animations/) It's exposed by [`UIAccessibility`' isReduceMotionEnabled ](https://developer.apple.com/documentation/uikit/uiaccessibility/1615133-isreducemotionenabled ) on iOS and [Settings.Global.TRANSITION_ANIMATION_SCALE](https://developer.android.com/reference/android/provider/Settings.Global#TRANSITION_ANIMATION_SCALE) on Android. Up until now, `AccessibilityInfo` only exposes screen reader flag. By adding this second accessibility option, it's a good opportunity to rename `fetch` method to an appropriate name, `isScreenReaderEnabled`, as well as rename `change` event to `screenReaderChanged`, which will make it clearer and more specific. (In case it's approved, a follow-up PR could exposes [more iOS acessibility flags](https://developer.apple.com/documentation/uikit/uiaccessibility), such as `isShakeToUndoEnabled`, `isReduceTransparencyEnabled`, `isGrayscaleEnabled`, `isInvertColorsEnabled`) (iOS code inspired by [phonegap-mobile-accessibility](https://github.com/phonegap/phonegap-mobile-accessibility). And Android by [Flutter](https://github.com/flutter/engine/blob/master/shell/platform/android/io/flutter/view/AccessibilityBridge.java )) Pull Request resolved: https://github.com/facebook/react-native/pull/23839 Differential Revision: D14406227 Pulled By: hramos fbshipit-source-id: adf43be84c488522bf1e29d862681220ad193883
245 lines
8.7 KiB
Objective-C
245 lines
8.7 KiB
Objective-C
/**
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
#import "RCTAccessibilityManager.h"
|
|
|
|
#import "RCTUIManager.h"
|
|
#import "RCTBridge.h"
|
|
#import "RCTConvert.h"
|
|
#import "RCTEventDispatcher.h"
|
|
#import "RCTLog.h"
|
|
|
|
NSString *const RCTAccessibilityManagerDidUpdateMultiplierNotification = @"RCTAccessibilityManagerDidUpdateMultiplierNotification";
|
|
|
|
static NSString *UIKitCategoryFromJSCategory(NSString *JSCategory)
|
|
{
|
|
static NSDictionary *map = nil;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
map = @{@"extraSmall": UIContentSizeCategoryExtraSmall,
|
|
@"small": UIContentSizeCategorySmall,
|
|
@"medium": UIContentSizeCategoryMedium,
|
|
@"large": UIContentSizeCategoryLarge,
|
|
@"extraLarge": UIContentSizeCategoryExtraLarge,
|
|
@"extraExtraLarge": UIContentSizeCategoryExtraExtraLarge,
|
|
@"extraExtraExtraLarge": UIContentSizeCategoryExtraExtraExtraLarge,
|
|
@"accessibilityMedium": UIContentSizeCategoryAccessibilityMedium,
|
|
@"accessibilityLarge": UIContentSizeCategoryAccessibilityLarge,
|
|
@"accessibilityExtraLarge": UIContentSizeCategoryAccessibilityExtraLarge,
|
|
@"accessibilityExtraExtraLarge": UIContentSizeCategoryAccessibilityExtraExtraLarge,
|
|
@"accessibilityExtraExtraExtraLarge": UIContentSizeCategoryAccessibilityExtraExtraExtraLarge};
|
|
});
|
|
return map[JSCategory];
|
|
}
|
|
|
|
@interface RCTAccessibilityManager ()
|
|
|
|
@property (nonatomic, copy) NSString *contentSizeCategory;
|
|
@property (nonatomic, assign) CGFloat multiplier;
|
|
|
|
@end
|
|
|
|
@implementation RCTAccessibilityManager
|
|
|
|
@synthesize bridge = _bridge;
|
|
@synthesize multipliers = _multipliers;
|
|
|
|
RCT_EXPORT_MODULE()
|
|
|
|
+ (BOOL)requiresMainQueueSetup
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
- (instancetype)init
|
|
{
|
|
if (self = [super init]) {
|
|
_multiplier = 1.0;
|
|
|
|
// TODO: can this be moved out of the startup path?
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(didReceiveNewContentSizeCategory:)
|
|
name:UIContentSizeCategoryDidChangeNotification
|
|
object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(didReceiveNewVoiceOverStatus:)
|
|
name:UIAccessibilityVoiceOverStatusChanged
|
|
object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(accessibilityAnnouncementDidFinish:)
|
|
name:UIAccessibilityAnnouncementDidFinishNotification
|
|
object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(reduceMotionStatusDidChange:)
|
|
name:UIAccessibilityReduceMotionStatusDidChangeNotification
|
|
object:nil];
|
|
|
|
self.contentSizeCategory = RCTSharedApplication().preferredContentSizeCategory;
|
|
_isReduceMotionEnabled = UIAccessibilityIsReduceMotionEnabled();
|
|
_isVoiceOverEnabled = UIAccessibilityIsVoiceOverRunning();
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
}
|
|
|
|
- (void)didReceiveNewContentSizeCategory:(NSNotification *)note
|
|
{
|
|
self.contentSizeCategory = note.userInfo[UIContentSizeCategoryNewValueKey];
|
|
}
|
|
|
|
- (void)didReceiveNewVoiceOverStatus:(__unused NSNotification *)notification
|
|
{
|
|
BOOL newIsVoiceOverEnabled = UIAccessibilityIsVoiceOverRunning();
|
|
if (_isVoiceOverEnabled != newIsVoiceOverEnabled) {
|
|
_isVoiceOverEnabled = newIsVoiceOverEnabled;
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
[_bridge.eventDispatcher sendDeviceEventWithName:@"voiceOverDidChange"
|
|
body:@(_isVoiceOverEnabled)];
|
|
#pragma clang diagnostic pop
|
|
}
|
|
}
|
|
|
|
- (void)accessibilityAnnouncementDidFinish:(__unused NSNotification *)notification
|
|
{
|
|
NSDictionary *userInfo = notification.userInfo;
|
|
// Response dictionary to populate the event with.
|
|
NSDictionary *response = @{@"announcement": userInfo[UIAccessibilityAnnouncementKeyStringValue],
|
|
@"success": userInfo[UIAccessibilityAnnouncementKeyWasSuccessful]};
|
|
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
[_bridge.eventDispatcher sendDeviceEventWithName:@"announcementDidFinish"
|
|
body:response];
|
|
#pragma clang diagnostic pop
|
|
}
|
|
|
|
- (void)reduceMotionStatusDidChange:(__unused NSNotification *)notification
|
|
{
|
|
BOOL newReduceMotionEnabled = UIAccessibilityIsReduceMotionEnabled();
|
|
if (_isReduceMotionEnabled != newReduceMotionEnabled) {
|
|
_isReduceMotionEnabled = newReduceMotionEnabled;
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
[_bridge.eventDispatcher sendDeviceEventWithName:@"reduceMotionDidChange"
|
|
body:@(_isReduceMotionEnabled)];
|
|
#pragma clang diagnostic pop
|
|
}
|
|
}
|
|
|
|
- (void)setContentSizeCategory:(NSString *)contentSizeCategory
|
|
{
|
|
if (_contentSizeCategory != contentSizeCategory) {
|
|
_contentSizeCategory = [contentSizeCategory copy];
|
|
[self invalidateMultiplier];
|
|
}
|
|
}
|
|
|
|
- (void)invalidateMultiplier
|
|
{
|
|
self.multiplier = [self multiplierForContentSizeCategory:_contentSizeCategory];
|
|
[[NSNotificationCenter defaultCenter] postNotificationName:RCTAccessibilityManagerDidUpdateMultiplierNotification object:self];
|
|
}
|
|
|
|
- (CGFloat)multiplierForContentSizeCategory:(NSString *)category
|
|
{
|
|
NSNumber *m = self.multipliers[category];
|
|
if (m.doubleValue <= 0.0) {
|
|
RCTLogError(@"Can't determinte multiplier for category %@. Using 1.0.", category);
|
|
m = @1.0;
|
|
}
|
|
return m.doubleValue;
|
|
}
|
|
|
|
- (void)setMultipliers:(NSDictionary<NSString *, NSNumber *> *)multipliers
|
|
{
|
|
if (_multipliers != multipliers) {
|
|
_multipliers = [multipliers copy];
|
|
[self invalidateMultiplier];
|
|
}
|
|
}
|
|
|
|
- (NSDictionary<NSString *, NSNumber *> *)multipliers
|
|
{
|
|
if (_multipliers == nil) {
|
|
_multipliers = @{UIContentSizeCategoryExtraSmall: @0.823,
|
|
UIContentSizeCategorySmall: @0.882,
|
|
UIContentSizeCategoryMedium: @0.941,
|
|
UIContentSizeCategoryLarge: @1.0,
|
|
UIContentSizeCategoryExtraLarge: @1.118,
|
|
UIContentSizeCategoryExtraExtraLarge: @1.235,
|
|
UIContentSizeCategoryExtraExtraExtraLarge: @1.353,
|
|
UIContentSizeCategoryAccessibilityMedium: @1.786,
|
|
UIContentSizeCategoryAccessibilityLarge: @2.143,
|
|
UIContentSizeCategoryAccessibilityExtraLarge: @2.643,
|
|
UIContentSizeCategoryAccessibilityExtraExtraLarge: @3.143,
|
|
UIContentSizeCategoryAccessibilityExtraExtraExtraLarge: @3.571};
|
|
}
|
|
return _multipliers;
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(setAccessibilityContentSizeMultipliers:(NSDictionary *)JSMultipliers)
|
|
{
|
|
NSMutableDictionary<NSString *, NSNumber *> *multipliers = [NSMutableDictionary new];
|
|
for (NSString *__nonnull JSCategory in JSMultipliers) {
|
|
NSNumber *m = [RCTConvert NSNumber:JSMultipliers[JSCategory]];
|
|
NSString *UIKitCategory = UIKitCategoryFromJSCategory(JSCategory);
|
|
multipliers[UIKitCategory] = m;
|
|
}
|
|
self.multipliers = multipliers;
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(setAccessibilityFocus:(nonnull NSNumber *)reactTag)
|
|
{
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
UIView *view = [self.bridge.uiManager viewForReactTag:reactTag];
|
|
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, view);
|
|
});
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(announceForAccessibility:(NSString *)announcement)
|
|
{
|
|
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcement);
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(getMultiplier:(RCTResponseSenderBlock)callback)
|
|
{
|
|
if (callback) {
|
|
callback(@[ @(self.multiplier) ]);
|
|
}
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(getCurrentVoiceOverState:(RCTResponseSenderBlock)callback
|
|
error:(__unused RCTResponseSenderBlock)error)
|
|
{
|
|
callback(@[@(_isVoiceOverEnabled)]);
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(getReduceMotionState:(RCTResponseSenderBlock)callback
|
|
error:(__unused RCTResponseSenderBlock)error)
|
|
{
|
|
callback(@[@(_isReduceMotionEnabled)]);
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation RCTBridge (RCTAccessibilityManager)
|
|
|
|
- (RCTAccessibilityManager *)accessibilityManager
|
|
{
|
|
return [self moduleForClass:[RCTAccessibilityManager class]];
|
|
}
|
|
|
|
@end
|