Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[General] [added] - Add support for "reduce motion" into AccessibilityInfo #23839

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 38 additions & 12 deletions Libraries/Components/AccessibilityInfo/AccessibilityInfo.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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') {
estevaolucas marked this conversation as resolved.
Show resolved Hide resolved
estevaolucas marked this conversation as resolved.
Show resolved Hide resolved
listener = RCTDeviceEventEmitter.addListener(
TOUCH_EXPLORATION_EVENT,
enabled => {
handler(enabled);
},
);
} else if (eventName === 'reduceMotionChanged') {
estevaolucas marked this conversation as resolved.
Show resolved Hide resolved
estevaolucas marked this conversation as resolved.
Show resolved Hide resolved
listener = RCTDeviceEventEmitter.addListener(
REDUCE_MOTION_EVENT,
enabled => {
handler(enabled);
},
);
}

_subscriptions.set(handler, listener);
},

Expand Down
49 changes: 42 additions & 7 deletions Libraries/Components/AccessibilityInfo/AccessibilityInfo.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions React/Modules/RCTAccessibilityManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions React/Modules/RCTAccessibilityManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -207,6 +226,12 @@ - (void)setMultipliers:(NSDictionary<NSString *, NSNumber *> *)multipliers
callback(@[@(_isVoiceOverEnabled)]);
}

RCT_EXPORT_METHOD(getReduceMotionState:(RCTResponseSenderBlock)callback
error:(__unused RCTResponseSenderBlock)error)
{
callback(@[@(_isReduceMotionEnabled)]);
}

@end

@implementation RCTBridge (RCTAccessibilityManager)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
}
Expand All @@ -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);
}
}

Expand All @@ -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
Expand All @@ -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
Expand Down