Skip to content

Commit

Permalink
Add support for reduce motion on Android
Browse files Browse the repository at this point in the history
  • Loading branch information
Estevão Lucas committed Mar 10, 2019
1 parent ab7175d commit da5a517
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 23 deletions.
48 changes: 38 additions & 10 deletions Libraries/Components/AccessibilityInfo/AccessibilityInfo.android.js
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 @@ -37,24 +40,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') {
const listener = RCTDeviceEventEmitter.addListener(
TOUCH_EXPLORATION_EVENT,
enabled => {
handler(enabled);
},
);
} else if (eventName === 'reduceMotionChanged') {
const listener = RCTDeviceEventEmitter.addListener(
REDUCE_MOTION_EVENT,
enabled => {
handler(enabled);
},
);
}

_subscriptions.set(handler, listener);
},

Expand Down
Expand Up @@ -80,8 +80,9 @@ const AccessibilityInfo = {
* Add an event handler. Supported events:
*
* - `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 screen
* reader is enabled and `false` otherwise.
* 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.
Expand Down
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

0 comments on commit da5a517

Please sign in to comment.