diff --git a/Libraries/Components/AccessibilityInfo/AccessibilityInfo.android.js b/Libraries/Components/AccessibilityInfo/AccessibilityInfo.android.js index 6cd0193eb42eca..455ba91263704d 100644 --- a/Libraries/Components/AccessibilityInfo/AccessibilityInfo.android.js +++ b/Libraries/Components/AccessibilityInfo/AccessibilityInfo.android.js @@ -16,10 +16,13 @@ const UIManager = require('UIManager'); const RCTAccessibilityInfo = NativeModules.AccessibilityInfo; +const REDUCE_MOTION_EVENT = 'reduceMotionDidChange'; const TOUCH_EXPLORATION_EVENT = 'touchExplorationDidChange'; type ChangeEventName = $Enum<{ change: string, + reduceMotionChanged: string, + screenReaderChanged: string, }>; const _subscriptions = new Map(); @@ -35,26 +38,49 @@ const _subscriptions = new Map(); */ const AccessibilityInfo = { - /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was found - * when making Flow check .android.js files. */ - fetch: function(): Promise { + isReduceMotionEnabled: function(): Promise { return new Promise((resolve, reject) => { - RCTAccessibilityInfo.isTouchExplorationEnabled(function(resp) { - resolve(resp); - }); + RCTAccessibilityInfo.isReduceMotionEnabled(resolve); }); }, + isScreenReaderEnabled: function(): Promise { + return new Promise((resolve, reject) => { + RCTAccessibilityInfo.isTouchExplorationEnabled(resolve); + }); + }, + + /** + * Deprecated + * + * Same as `isScreenReaderEnabled` + */ + get fetch() { + return this.isScreenReaderEnabled; + }, + addEventListener: function( eventName: ChangeEventName, handler: Function, ): void { - const listener = RCTDeviceEventEmitter.addListener( - TOUCH_EXPLORATION_EVENT, - enabled => { - handler(enabled); - }, - ); + let listener; + + if (eventName === 'change' || eventName === 'screenReaderChanged') { + listener = RCTDeviceEventEmitter.addListener( + TOUCH_EXPLORATION_EVENT, + enabled => { + handler(enabled); + }, + ); + } else if (eventName === 'reduceMotionChanged') { + listener = RCTDeviceEventEmitter.addListener( + REDUCE_MOTION_EVENT, + enabled => { + handler(enabled); + }, + ); + } + _subscriptions.set(handler, listener); }, diff --git a/Libraries/Components/AccessibilityInfo/AccessibilityInfo.ios.js b/Libraries/Components/AccessibilityInfo/AccessibilityInfo.ios.js index 5a1d64e9595e83..51ebadbbc045ce 100644 --- a/Libraries/Components/AccessibilityInfo/AccessibilityInfo.ios.js +++ b/Libraries/Components/AccessibilityInfo/AccessibilityInfo.ios.js @@ -16,12 +16,15 @@ const RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); const AccessibilityManager = NativeModules.AccessibilityManager; -const VOICE_OVER_EVENT = 'voiceOverDidChange'; const ANNOUNCEMENT_DID_FINISH_EVENT = 'announcementDidFinish'; +const REDUCE_MOTION_EVENT = 'reduceMotionDidChange'; +const VOICE_OVER_EVENT = 'voiceOverDidChange'; type ChangeEventName = $Enum<{ - change: string, announcementFinished: string, + change: string, + reduceMotionChanged: string, + screenReaderChanged: string, }>; const _subscriptions = new Map(); @@ -37,23 +40,50 @@ const _subscriptions = new Map(); */ const AccessibilityInfo = { /** - * Query whether a screen reader is currently enabled. + * Query whether a reduce motion is currently enabled. * * Returns a promise which resolves to a boolean. * The result is `true` when a screen reader is enabledand `false` otherwise. * - * See http://facebook.github.io/react-native/docs/accessibilityinfo.html#fetch + * See http://facebook.github.io/react-native/docs/accessibilityinfo.html#isReduceMotionEnabled */ - fetch: function(): Promise { + isReduceMotionEnabled: function(): Promise { + return new Promise((resolve, reject) => { + AccessibilityManager.getReduceMotionState(resolve, reject); + }); + }, + + /** + * Query whether a screen reader is currently enabled. + * + * Returns a promise which resolves to a boolean. + * The result is `true` when a screen reader is enabled and `false` otherwise. + * + * See http://facebook.github.io/react-native/docs/accessibilityinfo.html#isScreenReaderEnabled + */ + isScreenReaderEnabled: function(): Promise { return new Promise((resolve, reject) => { AccessibilityManager.getCurrentVoiceOverState(resolve, reject); }); }, + /** + * Deprecated + * + * Same as `isScreenReaderEnabled` + */ + get fetch() { + return this.isScreenReaderEnabled; + }, + /** * Add an event handler. Supported events: * - * - `change`: Fires when the state of the screen reader changes. The argument + * - `reduceMotionChanged`: Fires when the state of the reduce motion toggle changes. + * The argument to the event handler is a boolean. The boolean is `true` when a reduce + * motion is enabled (or when "Transition Animation Scale" in "Developer options" is + * "Animation off") and `false` otherwise. + * - `screenReaderChanged`: Fires when the state of the screen reader changes. The argument * to the event handler is a boolean. The boolean is `true` when a screen * reader is enabled and `false` otherwise. * - `announcementFinished`: iOS-only event. Fires when the screen reader has @@ -71,8 +101,13 @@ const AccessibilityInfo = { ): Object { let listener; - if (eventName === 'change') { + if (eventName === 'change' || eventName === 'screenReaderChanged') { listener = RCTDeviceEventEmitter.addListener(VOICE_OVER_EVENT, handler); + } else if (eventName === 'reduceMotionChanged') { + listener = RCTDeviceEventEmitter.addListener( + REDUCE_MOTION_EVENT, + handler, + ); } else if (eventName === 'announcementFinished') { listener = RCTDeviceEventEmitter.addListener( ANNOUNCEMENT_DID_FINISH_EVENT, diff --git a/React/Modules/RCTAccessibilityManager.h b/React/Modules/RCTAccessibilityManager.h index 35f03280e47f3c..c0d286f2088b93 100644 --- a/React/Modules/RCTAccessibilityManager.h +++ b/React/Modules/RCTAccessibilityManager.h @@ -19,6 +19,7 @@ extern NSString *const RCTAccessibilityManagerDidUpdateMultiplierNotification; / /// map from UIKit categories to multipliers @property (nonatomic, copy) NSDictionary *multipliers; +@property (nonatomic, assign) BOOL isReduceMotionEnabled; @property (nonatomic, assign) BOOL isVoiceOverEnabled; @end diff --git a/React/Modules/RCTAccessibilityManager.m b/React/Modules/RCTAccessibilityManager.m index 74adf9e41a758c..a1c7746ce171d7 100644 --- a/React/Modules/RCTAccessibilityManager.m +++ b/React/Modules/RCTAccessibilityManager.m @@ -76,7 +76,13 @@ - (instancetype)init 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; @@ -119,6 +125,19 @@ - (void)accessibilityAnnouncementDidFinish:(__unused NSNotification *)notificati #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) { @@ -207,6 +226,12 @@ - (void)setMultipliers:(NSDictionary *)multipliers callback(@[@(_isVoiceOverEnabled)]); } +RCT_EXPORT_METHOD(getReduceMotionState:(RCTResponseSenderBlock)callback + error:(__unused RCTResponseSenderBlock)error) +{ + callback(@[@(_isReduceMotionEnabled)]); +} + @end @implementation RCTBridge (RCTAccessibilityManager) diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/accessibilityinfo/AccessibilityInfoModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/accessibilityinfo/AccessibilityInfoModule.java index 0e68c5b31ba6f9..899f3d0e32dfbe 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/accessibilityinfo/AccessibilityInfoModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/accessibilityinfo/AccessibilityInfoModule.java @@ -9,7 +9,13 @@ import android.annotation.TargetApi; import android.content.Context; +import android.content.ContentResolver; +import android.database.ContentObserver; +import android.net.Uri; import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.provider.Settings; import android.view.accessibility.AccessibilityManager; import com.facebook.react.bridge.Callback; @@ -36,21 +42,42 @@ private class ReactTouchExplorationStateChangeListener @Override public void onTouchExplorationStateChanged(boolean enabled) { - updateAndSendChangeEvent(enabled); + updateAndSendTouchExplorationChangeEvent(enabled); } } + // Listener that is notified when the global TRANSITION_ANIMATION_SCALE. + private final ContentObserver animationScaleObserver = new ContentObserver(new Handler(Looper.getMainLooper())) { + @Override + public void onChange(boolean selfChange) { + this.onChange(selfChange, null); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + if (getReactApplicationContext().hasActiveCatalystInstance()) { + AccessibilityInfoModule.this.updateAndSendReduceMotionChangeEvent(); + } + } + }; + private @Nullable AccessibilityManager mAccessibilityManager; private @Nullable ReactTouchExplorationStateChangeListener mTouchExplorationStateChangeListener; - private boolean mEnabled = false; + private final ContentResolver mContentResolver; + private boolean mReduceMotionEnabled = false; + private boolean mTouchExplorationEnabled = false; - private static final String EVENT_NAME = "touchExplorationDidChange"; + private static final String REDUCE_MOTION_EVENT_NAME = "reduceMotionDidChange"; + private static final String TOUCH_EXPLORATION_EVENT_NAME = "touchExplorationDidChange"; public AccessibilityInfoModule(ReactApplicationContext context) { super(context); Context appContext = context.getApplicationContext(); mAccessibilityManager = (AccessibilityManager) appContext.getSystemService(Context.ACCESSIBILITY_SERVICE); - mEnabled = mAccessibilityManager.isTouchExplorationEnabled(); + mContentResolver = getReactApplicationContext().getContentResolver(); + mTouchExplorationEnabled = mAccessibilityManager.isTouchExplorationEnabled(); + mReduceMotionEnabled = this.getIsReduceMotionEnabledValue(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { mTouchExplorationStateChangeListener = new ReactTouchExplorationStateChangeListener(); } @@ -61,16 +88,41 @@ public String getName() { return "AccessibilityInfo"; } + private boolean getIsReduceMotionEnabledValue() { + String value = Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1 ? null + : Settings.Global.getString( + mContentResolver, + Settings.Global.TRANSITION_ANIMATION_SCALE + ); + + return value != null && value.equals("0.0"); + } + + @ReactMethod + public void isReduceMotionEnabled(Callback successCallback) { + successCallback.invoke(mReduceMotionEnabled); + } + @ReactMethod public void isTouchExplorationEnabled(Callback successCallback) { - successCallback.invoke(mEnabled); + successCallback.invoke(mTouchExplorationEnabled); + } + + private void updateAndSendReduceMotionChangeEvent() { + boolean isReduceMotionEnabled = this.getIsReduceMotionEnabledValue(); + + if (mReduceMotionEnabled != isReduceMotionEnabled) { + mReduceMotionEnabled = isReduceMotionEnabled; + getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit(REDUCE_MOTION_EVENT_NAME, mReduceMotionEnabled); + } } - private void updateAndSendChangeEvent(boolean enabled) { - if (mEnabled != enabled) { - mEnabled = enabled; + private void updateAndSendTouchExplorationChangeEvent(boolean enabled) { + if (mTouchExplorationEnabled != enabled) { + mTouchExplorationEnabled = enabled; getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) - .emit(EVENT_NAME, mEnabled); + .emit(TOUCH_EXPLORATION_EVENT_NAME, mTouchExplorationEnabled); } } @@ -80,7 +132,14 @@ public void onHostResume() { mAccessibilityManager.addTouchExplorationStateChangeListener( mTouchExplorationStateChangeListener); } - updateAndSendChangeEvent(mAccessibilityManager.isTouchExplorationEnabled()); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + Uri transitionUri = Settings.Global.getUriFor(Settings.Global.TRANSITION_ANIMATION_SCALE); + mContentResolver.registerContentObserver(transitionUri, false, animationScaleObserver); + } + + updateAndSendTouchExplorationChangeEvent(mAccessibilityManager.isTouchExplorationEnabled()); + updateAndSendReduceMotionChangeEvent(); } @Override @@ -89,12 +148,17 @@ public void onHostPause() { mAccessibilityManager.removeTouchExplorationStateChangeListener( mTouchExplorationStateChangeListener); } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + mContentResolver.unregisterContentObserver(animationScaleObserver); + } } @Override public void initialize() { getReactApplicationContext().addLifecycleEventListener(this); - updateAndSendChangeEvent(mAccessibilityManager.isTouchExplorationEnabled()); + updateAndSendTouchExplorationChangeEvent(mAccessibilityManager.isTouchExplorationEnabled()); + updateAndSendReduceMotionChangeEvent(); } @Override