mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-01-12 22:50:10 +08:00
- Add support for "reduce motion" into AccessibilityInfo (#23839)
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
This commit is contained in:
committed by
Facebook Github Bot
parent
8e490d4d87
commit
0090ab32c2
@@ -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<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
RCTAccessibilityInfo.isTouchExplorationEnabled(function(resp) {
|
||||
resolve(resp);
|
||||
});
|
||||
RCTAccessibilityInfo.isReduceMotionEnabled(resolve);
|
||||
});
|
||||
},
|
||||
|
||||
isScreenReaderEnabled: function(): Promise<boolean> {
|
||||
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);
|
||||
},
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -19,6 +19,7 @@ extern NSString *const RCTAccessibilityManagerDidUpdateMultiplierNotification; /
|
||||
/// map from UIKit categories to multipliers
|
||||
@property (nonatomic, copy) NSDictionary<NSString *, NSNumber *> *multipliers;
|
||||
|
||||
@property (nonatomic, assign) BOOL isReduceMotionEnabled;
|
||||
@property (nonatomic, assign) BOOL isVoiceOverEnabled;
|
||||
|
||||
@end
|
||||
|
||||
@@ -76,7 +76,13 @@ RCT_EXPORT_MODULE()
|
||||
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 @@ RCT_EXPORT_MODULE()
|
||||
#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 @@ RCT_EXPORT_METHOD(getCurrentVoiceOverState:(RCTResponseSenderBlock)callback
|
||||
callback(@[@(_isVoiceOverEnabled)]);
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(getReduceMotionState:(RCTResponseSenderBlock)callback
|
||||
error:(__unused RCTResponseSenderBlock)error)
|
||||
{
|
||||
callback(@[@(_isReduceMotionEnabled)]);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation RCTBridge (RCTAccessibilityManager)
|
||||
|
||||
@@ -9,7 +9,13 @@ import javax.annotation.Nullable;
|
||||
|
||||
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 @@ public class AccessibilityInfoModule extends ReactContextBaseJavaModule
|
||||
|
||||
@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 class AccessibilityInfoModule extends ReactContextBaseJavaModule
|
||||
return "AccessibilityInfo";
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void isTouchExplorationEnabled(Callback successCallback) {
|
||||
successCallback.invoke(mEnabled);
|
||||
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");
|
||||
}
|
||||
|
||||
private void updateAndSendChangeEvent(boolean enabled) {
|
||||
if (mEnabled != enabled) {
|
||||
mEnabled = enabled;
|
||||
@ReactMethod
|
||||
public void isReduceMotionEnabled(Callback successCallback) {
|
||||
successCallback.invoke(mReduceMotionEnabled);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void isTouchExplorationEnabled(Callback successCallback) {
|
||||
successCallback.invoke(mTouchExplorationEnabled);
|
||||
}
|
||||
|
||||
private void updateAndSendReduceMotionChangeEvent() {
|
||||
boolean isReduceMotionEnabled = this.getIsReduceMotionEnabledValue();
|
||||
|
||||
if (mReduceMotionEnabled != isReduceMotionEnabled) {
|
||||
mReduceMotionEnabled = isReduceMotionEnabled;
|
||||
getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||
.emit(EVENT_NAME, mEnabled);
|
||||
.emit(REDUCE_MOTION_EVENT_NAME, mReduceMotionEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateAndSendTouchExplorationChangeEvent(boolean enabled) {
|
||||
if (mTouchExplorationEnabled != enabled) {
|
||||
mTouchExplorationEnabled = enabled;
|
||||
getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||
.emit(TOUCH_EXPLORATION_EVENT_NAME, mTouchExplorationEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +132,14 @@ public class AccessibilityInfoModule extends ReactContextBaseJavaModule
|
||||
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 class AccessibilityInfoModule extends ReactContextBaseJavaModule
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user