From cd1d652af4ebed123dbbee1d015ba3835ec33356 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Tue, 23 Aug 2016 13:59:07 -0700 Subject: [PATCH] Add AnimatedDiffClamp node Summary: This adds a new type of node that clamps an animated value between 2 values with a special twist, it is based on the difference between the previous value so getting far from a bound doesn't matter and as soon as we start getting closer again the value will start changing. The main use case for this node is to create a collapsible navbar when scrolling a scrollview. This is a pretty in apps (fb, youtube, twitter, all use something like this). It updates using the following: `value = clamp(value + diff, min, max)` where `diff` is the difference with the previous value. This gives the following output for parameters min = 0, max = 30: ``` in out 0 0 15 15 30 30 100 30 90 20 30 0 50 20 ``` One issue I see is that this node is pretty specific to this use case but I can't see another simple way to do this with Animated that can also be offloaded to native easily. I'd be glad to discuss other solutions if some Closes https://github.com/facebook/react-native/pull/9419 Differential Revision: D3753920 fbshipit-source-id: 40a749d38fd003aab2d3cb5cb8f0535e467d8a2a --- .../Animated/src/AnimatedImplementation.js | 56 +++++++++++++++++++ .../Animated/src/__tests__/Animated-test.js | 13 +++++ 2 files changed, 69 insertions(+) diff --git a/Libraries/Animated/src/AnimatedImplementation.js b/Libraries/Animated/src/AnimatedImplementation.js index 05b7ea05fc240b..dfc404e83bb15b 100644 --- a/Libraries/Animated/src/AnimatedImplementation.js +++ b/Libraries/Animated/src/AnimatedImplementation.js @@ -1193,6 +1193,43 @@ class AnimatedModulo extends AnimatedWithChildren { } } +class AnimatedDiffClamp extends AnimatedWithChildren { + _a: Animated; + _min: number; + _max: number; + _value: number; + _lastValue: number; + + constructor(a: Animated, min: number, max: number) { + super(); + + this._a = a; + this._min = min; + this._max = max; + this._value = this._lastValue = this._a.__getValue(); + } + + interpolate(config: InterpolationConfigType): AnimatedInterpolation { + return new AnimatedInterpolation(this, config); + } + + __getValue(): number { + const value = this._a.__getValue(); + const diff = value - this._lastValue; + this._lastValue = value; + this._value = Math.min(Math.max(this._value + diff, this._min), this._max); + return this._value; + } + + __attach(): void { + this._a.__addChild(this); + } + + __detach(): void { + this._a.__removeChild(this); + } +} + class AnimatedTransform extends AnimatedWithChildren { _transforms: Array; @@ -1693,6 +1730,14 @@ var modulo = function( return new AnimatedModulo(a, modulus); }; +var diffClamp = function( + a: Animated, + min: number, + max: number, +): AnimatedDiffClamp { + return new AnimatedDiffClamp(a, min, max); +}; + const _combineCallbacks = function(callback: ?EndCallback, config : AnimationConfig) { if (callback && config.onComplete) { return (...args) => { @@ -2084,6 +2129,17 @@ module.exports = { */ modulo, + /** + * Create a new Animated value that is limited between 2 values. It uses the + * difference between the last value so even if the value is far from the bounds + * it will start changing when the value starts getting closer again. + * (`value = clamp(value + diff, min, max)`). + * + * This is useful with scroll events, for example, to show the navbar when + * scrolling up and to hide it when scrolling down. + */ + diffClamp, + /** * Starts an animation after the given delay. */ diff --git a/Libraries/Animated/src/__tests__/Animated-test.js b/Libraries/Animated/src/__tests__/Animated-test.js index 02cb505755dab4..d57f9b672448cb 100644 --- a/Libraries/Animated/src/__tests__/Animated-test.js +++ b/Libraries/Animated/src/__tests__/Animated-test.js @@ -567,3 +567,16 @@ describe('Animated Listeners', () => { expect(listener.mock.calls.length).toBe(4); }); }); + +describe('Animated Diff Clamp', () => { + it('should get the proper value', () => { + const inputValues = [0, 20, 40, 30, 0, -40, -10, -20, 0]; + const expectedValues = [0, 20, 20, 10, 0, 0, 20, 10, 20]; + const value = new Animated.Value(0); + const diffClampValue = Animated.diffClamp(value, 0, 20); + for (let i = 0; i < inputValues.length; i++) { + value.setValue(inputValues[i]); + expect(diffClampValue.__getValue()).toBe(expectedValues[i]); + } + }); +});