Skip to content

Commit d98a731

Browse files
committed
feat: support 120fps for Animated on iOS
1 parent bf12e30 commit d98a731

35 files changed

Lines changed: 321 additions & 97 deletions

packages/react-native/Libraries/Animated/animations/TimingAnimation.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,22 @@ import type AnimatedValue from '../nodes/AnimatedValue';
1515
import type AnimatedValueXY from '../nodes/AnimatedValueXY';
1616
import type {AnimationConfig, EndCallback} from './Animation';
1717

18+
import NativeAnimatedModule from '../NativeAnimatedModule';
19+
import NativeAnimatedTurboModule from '../NativeAnimatedTurboModule';
1820
import AnimatedColor from '../nodes/AnimatedColor';
1921
import Animation from './Animation';
2022

23+
const Platform = require('../Utilities/Platform').default;
24+
25+
const singleFrameInterval =
26+
Platform.OS === 'ios'
27+
? NativeAnimatedTurboModule?.getConstants().singleFrameInterval
28+
: NativeAnimatedModule?.getConstants().singleFrameInterval;
29+
30+
const frameDuration = singleFrameInterval
31+
? singleFrameInterval * 1000
32+
: 1000.0 / 60.0;
33+
2134
export type TimingAnimationConfig = $ReadOnly<{
2235
...AnimationConfig,
2336
toValue:
@@ -88,7 +101,6 @@ export default class TimingAnimation extends Animation {
88101
platformConfig: ?PlatformConfig,
89102
...
90103
}> {
91-
const frameDuration = 1000.0 / 60.0;
92104
const frames = [];
93105
const numFrames = Math.round(this._duration / frameDuration);
94106
for (let frame = 0; frame < numFrames; frame++) {

packages/react-native/Libraries/NativeAnimation/Drivers/RCTAnimationDriver.h

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,30 @@
99
#import <Foundation/Foundation.h>
1010

1111
#import <React/RCTBridgeModule.h>
12-
13-
static CGFloat RCTSingleFrameInterval = (CGFloat)(1.0 / 60.0);
12+
#import <React/RCTUtils.h>
13+
14+
#ifdef __cplusplus
15+
#include <react/featureflags/ReactNativeFeatureFlags.h>
16+
17+
static CGFloat RCTSingleFrameInterval(void)
18+
{
19+
if (facebook::react::ReactNativeFeatureFlags::disableHighRefreshRateAnimations()) {
20+
// Fallback to 60 fps if disabled.
21+
return 1.0 / 60;
22+
}
23+
24+
static CGFloat maximumFramesPerSecond;
25+
static dispatch_once_t onceToken;
26+
dispatch_once(&onceToken, ^{
27+
RCTUnsafeExecuteOnMainQueueSync(^{
28+
maximumFramesPerSecond = [UIScreen mainScreen].maximumFramesPerSecond;
29+
});
30+
});
31+
32+
return 1.0 / maximumFramesPerSecond;
33+
};
34+
35+
#endif
1436

1537
@class RCTValueAnimatedNode;
1638

packages/react-native/Libraries/NativeAnimation/Drivers/RCTDecayAnimation.mm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ - (void)stepAnimationWithTime:(NSTimeInterval)currentTime
8585

8686
if (_frameStartTime == -1) {
8787
// Since this is the first animation step, consider the start to be on the previous frame.
88-
_frameStartTime = currentTime - RCTSingleFrameInterval;
88+
_frameStartTime = currentTime - RCTSingleFrameInterval();
8989
if (_fromValue == _lastValue) {
9090
// First iteration, assign _fromValue based on _valueNode.
9191
_fromValue = _valueNode.value;

packages/react-native/Libraries/NativeAnimation/Drivers/RCTFrameAnimation.mm

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,12 @@ - (void)stepAnimationWithTime:(NSTimeInterval)currentTime
9595

9696
_animationCurrentTime = currentTime;
9797
NSTimeInterval currentDuration = (_animationCurrentTime - _animationStartTime) / RCTAnimationDragCoefficient();
98+
99+
CGFloat singleFrameInterval = RCTSingleFrameInterval();
98100

99101
// Determine how many frames have passed since last update.
100102
// Get index of frames that surround the current interval
101-
NSUInteger startIndex = floor(currentDuration / RCTSingleFrameInterval);
103+
NSUInteger startIndex = floor(currentDuration / singleFrameInterval);
102104
NSUInteger nextIndex = startIndex + 1;
103105

104106
if (nextIndex >= _frames.count) {
@@ -121,8 +123,8 @@ - (void)stepAnimationWithTime:(NSTimeInterval)currentTime
121123
// Do a linear remap of the two frames to safeguard against variable framerates
122124
NSNumber *fromFrameValue = _frames[startIndex];
123125
NSNumber *toFrameValue = _frames[nextIndex];
124-
NSTimeInterval fromInterval = (double)startIndex * RCTSingleFrameInterval;
125-
NSTimeInterval toInterval = (double)nextIndex * RCTSingleFrameInterval;
126+
NSTimeInterval fromInterval = (double)startIndex * singleFrameInterval;
127+
NSTimeInterval toInterval = (double)nextIndex * singleFrameInterval;
126128

127129
// Interpolate between the individual frames to ensure the animations are
128130
// smooth and of the proper duration regardless of the framerate.

packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.mm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,7 @@ - (void)startAnimationLoopIfNeeded
441441
{
442442
if (!_displayLink && _activeAnimations.count > 0) {
443443
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(stepAnimations:)];
444+
_displayLink.preferredFramesPerSecond = [UIScreen mainScreen].maximumFramesPerSecond;
444445
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
445446
}
446447
}

packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedTurboModule.mm

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@
1111
#import <React/RCTNativeAnimatedNodesManager.h>
1212
#import <React/RCTNativeAnimatedTurboModule.h>
1313
#import <react/featureflags/ReactNativeFeatureFlags.h>
14+
#import <React/RCTAnimationDriver.h>
1415

1516
#import "RCTAnimationPlugins.h"
1617

18+
using namespace facebook::react;
19+
1720
typedef void (^AnimatedOperation)(RCTNativeAnimatedNodesManager *nodesManager);
1821

1922
@interface RCTNativeAnimatedTurboModule () <NativeAnimatedModuleSpec, RCTInitializing>
@@ -28,13 +31,14 @@ @implementation RCTNativeAnimatedTurboModule {
2831
NSMutableArray<AnimatedOperation> *_preOperations;
2932

3033
NSSet<NSString *> *_userDrivenAnimationEndedEvents;
34+
ModuleConstants<JS::NativeAnimatedModule::Constants> _constants;
3135
}
3236

3337
RCT_EXPORT_MODULE();
3438

3539
+ (BOOL)requiresMainQueueSetup
3640
{
37-
return NO;
41+
return YES;
3842
}
3943

4044
- (instancetype)init
@@ -43,6 +47,10 @@ - (instancetype)init
4347
_operations = [NSMutableArray new];
4448
_preOperations = [NSMutableArray new];
4549
_userDrivenAnimationEndedEvents = [NSSet setWithArray:@[ @"onScrollEnded" ]];
50+
51+
_constants = typedConstants<JS::NativeAnimatedModule::Constants>({
52+
.singleFrameInterval = RCTSingleFrameInterval()
53+
});
4654
}
4755
return self;
4856
}
@@ -371,6 +379,18 @@ - (void)eventDispatcherWillDispatchEvent:(id<RCTEvent>)event
371379
});
372380
}
373381

382+
#pragma mark -- Constants
383+
384+
- (ModuleConstants<JS::NativeAnimatedModule::Constants>)constantsToExport
385+
{
386+
return _constants;
387+
}
388+
389+
- (ModuleConstants<JS::NativeAnimatedModule::Constants>)getConstants
390+
{
391+
return _constants;
392+
}
393+
374394
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
375395
(const facebook::react::ObjCTurboModule::InitParams &)params
376396
{

packages/react-native/React/CoreModules/RCTFPSGraph.mm

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ - (void)onTick:(NSTimeInterval)timestamp
100100
});
101101

102102
CGFloat previousScale = _scale;
103-
CGFloat targetFps = MAX(_maxFPS, 60.0);
103+
CGFloat screenRefreshRate = [UIScreen mainScreen].maximumFramesPerSecond;
104+
CGFloat targetFps = MAX(_maxFPS, screenRefreshRate);
104105
_scale = targetFps / (CGFloat)_height;
105106
for (NSUInteger i = 0; i < _length - 1; i++) {
106107
// Move each Frame back one position and adjust to new scale (if there is a new scale)

packages/react-native/React/CoreModules/RCTPerfMonitor.mm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ - (void)show
300300
[RCTKeyWindow() addSubview:self.container];
301301

302302
_uiDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(threadUpdate:)];
303+
_uiDisplayLink.preferredFramesPerSecond = [UIScreen mainScreen].maximumFramesPerSecond;
303304
[_uiDisplayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
304305

305306
self.container.frame =

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/AnimationDriver.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,20 @@ package com.facebook.react.animated
1010
import com.facebook.react.bridge.Callback
1111
import com.facebook.react.bridge.JSApplicationCausedNativeException
1212
import com.facebook.react.bridge.ReadableMap
13+
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
14+
import android.content.Context
15+
import android.view.WindowManager
16+
17+
public fun getSingleFrameInterval(context: Context): Double {
18+
if (ReactNativeFeatureFlags.disableHighRefreshRateAnimations()) {
19+
return 60.0
20+
}
21+
22+
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
23+
24+
val refreshRate = windowManager.defaultDisplay.supportedRefreshRates.maxOrNull()
25+
return refreshRate?.toDouble() ?: 60.0
26+
}
1327

1428
/**
1529
* Base class for different types of animation drivers. Can be used to implement simple time-based

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/FrameBasedAnimationDriver.kt

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,23 @@
77

88
package com.facebook.react.animated
99

10+
import android.content.Context
1011
import com.facebook.common.logging.FLog
12+
import com.facebook.react.bridge.ReactApplicationContext
1113
import com.facebook.react.bridge.ReadableMap
1214
import com.facebook.react.bridge.ReadableType
1315
import com.facebook.react.common.ReactConstants
1416
import com.facebook.react.common.build.ReactBuildConfig
17+
import kotlin.math.roundToInt
1518

1619
/**
1720
* Implementation of [AnimationDriver] which provides a support for simple time-based animations
1821
* that are pre-calculate on the JS side. For each animation frame JS provides a value from 0 to 1
1922
* that indicates a progress of the animation at that frame.
2023
*/
21-
internal class FrameBasedAnimationDriver(config: ReadableMap) : AnimationDriver() {
24+
internal class FrameBasedAnimationDriver(config: ReadableMap,
25+
private var context: ReactApplicationContext?
26+
) : AnimationDriver() {
2227
private var startFrameTimeNanos: Long = -1
2328
private var frames: DoubleArray = DoubleArray(0)
2429
private var toValue = 0.0
@@ -62,7 +67,8 @@ internal class FrameBasedAnimationDriver(config: ReadableMap) : AnimationDriver(
6267
}
6368
}
6469
val timeFromStartMillis = (frameTimeNanos - startFrameTimeNanos) / 1000000
65-
val frameIndex = Math.round(timeFromStartMillis / FRAME_TIME_MILLIS).toInt()
70+
val frameTime = getFrameTimeMillis(context)
71+
val frameIndex = (timeFromStartMillis / frameTime).roundToInt()
6672
if (frameIndex < 0) {
6773
val message =
6874
("Calculated frame index should never be lower than 0. Called with frameTimeNanos " +
@@ -98,7 +104,11 @@ internal class FrameBasedAnimationDriver(config: ReadableMap) : AnimationDriver(
98104
}
99105

100106
companion object {
101-
// 60FPS
102-
private const val FRAME_TIME_MILLIS = 1000.0 / 60.0
107+
fun getFrameTimeMillis(context: Context?): Double {
108+
if (context == null) {
109+
return 1000.0 / 60.0
110+
}
111+
return 1000.0 / getSingleFrameInterval(context)
112+
}
103113
}
104114
}

0 commit comments

Comments
 (0)