`isInteraction` option to keep InteractionManager unblocked

Summary: Adds a new option to animation configs so that animations can be marked as non-interactions. This solves the "loading indicator problem" where an animated loading screen would deadlock as it waits for all interactions to complete.

As for the API, what we really want long term is better scheduling. This needs to be built at the runtime level (into the RN bridge) and take care of things like priority donation that are probably overkill right now. Basically this API is a medium-term fix and I won't be upset if it gets replaced with a finer-grained scheduler.
Closes https://github.com/facebook/react-native/pull/3433

Reviewed By: @​svcscm

Differential Revision: D2545300

Pulled By: @vjeux

fb-gh-sync-id: c1216bf69bfbbbae00c76c4697183fe96a6a01dd
This commit is contained in:
James Ide 2015-10-15 08:15:28 -07:00 committed by facebook-github-bot-7
parent 1e52b8297c
commit c38d7fab97
2 changed files with 70 additions and 8 deletions

View File

@ -40,11 +40,16 @@ class Animated {
__getChildren(): Array<Animated> { return []; }
}
type AnimationConfig = {
isInteraction?: bool;
};
// Important note: start() and stop() will only be called at most once.
// Once an animation has been stopped or finished its course, it will
// not be reused.
class Animation {
__active: bool;
__isInteraction: bool;
__onEnd: ?EndCallback;
start(
fromValue: number,
@ -128,14 +133,14 @@ function _flush(rootNode: AnimatedValue): void {
animatedStyles.forEach(animatedStyle => animatedStyle.update());
}
type TimingAnimationConfig = {
type TimingAnimationConfig = AnimationConfig & {
toValue: number | AnimatedValue | {x: number, y: number} | AnimatedValueXY;
easing?: (value: number) => number;
duration?: number;
delay?: number;
};
type TimingAnimationConfigSingle = {
type TimingAnimationConfigSingle = AnimationConfig & {
toValue: number | AnimatedValue;
easing?: (value: number) => number;
duration?: number;
@ -163,6 +168,7 @@ class TimingAnimation extends Animation {
this._easing = config.easing || easeInOut;
this._duration = config.duration !== undefined ? config.duration : 500;
this._delay = config.delay || 0;
this.__isInteraction = config.isInteraction !== undefined ? config.isInteraction : true;
}
start(
@ -223,12 +229,12 @@ class TimingAnimation extends Animation {
}
}
type DecayAnimationConfig = {
type DecayAnimationConfig = AnimationConfig & {
velocity: number | {x: number, y: number};
deceleration?: number;
};
type DecayAnimationConfigSingle = {
type DecayAnimationConfigSingle = AnimationConfig & {
velocity: number;
deceleration?: number;
};
@ -248,6 +254,7 @@ class DecayAnimation extends Animation {
super();
this._deceleration = config.deceleration || 0.998;
this._velocity = config.velocity;
this.__isInteraction = config.isInteraction !== undefined ? config.isInteraction : true;
}
start(
@ -291,7 +298,7 @@ class DecayAnimation extends Animation {
}
}
type SpringAnimationConfig = {
type SpringAnimationConfig = AnimationConfig & {
toValue: number | AnimatedValue | {x: number, y: number} | AnimatedValueXY;
overshootClamping?: bool;
restDisplacementThreshold?: number;
@ -303,7 +310,7 @@ type SpringAnimationConfig = {
friction?: number;
};
type SpringAnimationConfigSingle = {
type SpringAnimationConfigSingle = AnimationConfig & {
toValue: number | AnimatedValue;
overshootClamping?: bool;
restDisplacementThreshold?: number;
@ -349,6 +356,7 @@ class SpringAnimation extends Animation {
this._initialVelocity = config.velocity;
this._lastVelocity = withDefault(config.velocity, 0);
this._toValue = config.toValue;
this.__isInteraction = config.isInteraction !== undefined ? config.isInteraction : true;
var springConfig;
if (config.bounciness !== undefined || config.speed !== undefined) {
@ -611,7 +619,10 @@ class AnimatedValue extends AnimatedWithChildren {
* class.
*/
animate(animation: Animation, callback: ?EndCallback): void {
var handle = InteractionManager.createInteractionHandle();
var handle = null;
if (animation.__isInteraction) {
handle = InteractionManager.createInteractionHandle();
}
var previousAnimation = this._animation;
this._animation && this._animation.stop();
this._animation = animation;
@ -622,7 +633,9 @@ class AnimatedValue extends AnimatedWithChildren {
},
(result) => {
this._animation = null;
InteractionManager.clearInteractionHandle(handle);
if (handle !== null) {
InteractionManager.clearInteractionHandle(handle);
}
callback && callback(result);
},
previousAnimation,

View File

@ -18,6 +18,7 @@ jest
var Animated = require('Animated');
describe('Animated', () => {
it('works end to end', () => {
var anim = new Animated.Value(0);
@ -352,6 +353,54 @@ describe('Animated Events', () => {
});
});
describe('Animated Interactions', () => {
/*eslint-disable no-shadow*/
var Animated;
/*eslint-enable*/
var InteractionManager;
beforeEach(() => {
jest.mock('InteractionManager');
Animated = require('Animated');
InteractionManager = require('InteractionManager');
});
afterEach(()=> {
jest.dontMock('InteractionManager');
});
it('registers an interaction by default', () => {
InteractionManager.createInteractionHandle.mockReturnValue(777);
var value = new Animated.Value(0);
var callback = jest.genMockFunction();
Animated.timing(value, {
toValue: 100,
duration: 100,
}).start(callback);
jest.runAllTimers();
expect(InteractionManager.createInteractionHandle).toBeCalled();
expect(InteractionManager.clearInteractionHandle).toBeCalledWith(777);
expect(callback).toBeCalledWith({finished: true});
});
it('does not register an interaction when specified', () => {
var value = new Animated.Value(0);
var callback = jest.genMockFunction();
Animated.timing(value, {
toValue: 100,
duration: 100,
isInteraction: false,
}).start(callback);
jest.runAllTimers();
expect(InteractionManager.createInteractionHandle).not.toBeCalled();
expect(InteractionManager.clearInteractionHandle).not.toBeCalled();
expect(callback).toBeCalledWith({finished: true});
});
});
describe('Animated Tracking', () => {
it('should track values', () => {
var value1 = new Animated.Value(0);