Skip to content
Open
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
12 changes: 12 additions & 0 deletions .github/workflows/test-kmp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ on:
- 'packages/react-native/ReactCommon/react/utils/ManagedObjectWrapper.*'
- 'packages/react-native/React/React-RCTFabric.podspec'
- 'packages/react-native/React-Core.podspec'
- 'packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/**'
- 'packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/**'
- 'packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/scroll/**'
- 'packages/react-native/React/Base/RCTMultipartStreamReader.*'
- 'packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/MultipartStreamReader.kt'
- 'packages/react-native/ReactAndroid/src/test/java/com/facebook/react/devsupport/MultipartStreamReaderTest.kt'
Expand Down Expand Up @@ -78,6 +81,9 @@ on:
- 'packages/react-native/ReactCommon/react/utils/ManagedObjectWrapper.*'
- 'packages/react-native/React/React-RCTFabric.podspec'
- 'packages/react-native/React-Core.podspec'
- 'packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/**'
- 'packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/**'
- 'packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/scroll/**'
- 'packages/react-native/React/Base/RCTMultipartStreamReader.*'
- 'packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/MultipartStreamReader.kt'
- 'packages/react-native/ReactAndroid/src/test/java/com/facebook/react/devsupport/MultipartStreamReaderTest.kt'
Expand Down Expand Up @@ -155,6 +161,10 @@ jobs:
run: |
./scripts/test-apple-multipart.sh
python3 scripts/test-android-multipart.py --max-workers 2
- name: Compare native and shared scroll snap adapters
if: ${{ !cancelled() && steps.shared.outcome == 'success' }}
working-directory: packages/react-native/ReactShared
run: python3 scripts/test-apple-scroll-snap.py --output "$RUNNER_TEMP/apple-scroll-snap" --benchmark-repeats 0
- name: Test packaged XCFramework consumption
if: ${{ !cancelled() && steps.shared.outcome == 'success' }}
working-directory: packages/react-native/ReactShared
Expand All @@ -179,6 +189,8 @@ jobs:
path: |
packages/react-native/ReactShared/build/reports/tests
packages/react-native/ReactShared/build/test-results
${{ runner.temp }}/apple-scroll-snap/**/*.json
${{ runner.temp }}/apple-scroll-snap/**/*.log
packages/react-native/ReactShared/build/apple-multipart-test/**/*.log
packages/react-native/ReactShared/build/android-multipart-test/build/test-results
packages/react-native/ReactShared/build/apple-distribution/**/*.json
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,44 @@

#import "RCTEnhancedScrollView.h"
#import <React/RCTUtils.h>
#import <TargetConditionals.h>
#import <react/utils/FloatComparison.h>

#if RCT_USE_KMP && TARGET_OS_IOS && !TARGET_OS_MACCATALYST
#define RCT_SCROLL_SNAP_USE_KMP 1
#import <ReactNativeShared/ReactNativeShared.h>
#else
#define RCT_SCROLL_SNAP_USE_KMP 0
#endif

@interface RCTEnhancedScrollView () <UIScrollViewDelegate>
@end

@implementation RCTEnhancedScrollView {
__weak id<UIScrollViewDelegate> _publicDelegate;
BOOL _isSetContentOffsetDisabled;
#if RCT_SCROLL_SNAP_USE_KMP
RNSScrollSnapOffsets *_sharedSnapOffsets;
#endif
}

#if RCT_SCROLL_SNAP_USE_KMP
@synthesize snapToOffsets = _snapToOffsets;

- (void)setSnapToOffsets:(NSArray<NSNumber *> *)snapToOffsets
{
_snapToOffsets = [snapToOffsets copy];
_sharedSnapOffsets = nil;
if (_snapToOffsets.count > 0) {
// Convert once per property update, retaining the existing floatValue precision.
RNSKotlinDoubleArray *offsets = [RNSKotlinDoubleArray arrayWithSize:(int32_t)_snapToOffsets.count];
for (NSUInteger i = 0; i < _snapToOffsets.count; i++) {
[offsets setIndex:(int32_t)i value:_snapToOffsets[i].floatValue];
}
_sharedSnapOffsets = [[RNSScrollSnapOffsets alloc] initWithOffsets:offsets];
}
}
#endif

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
Expand Down Expand Up @@ -199,6 +228,15 @@ - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView

// Calculate the snap offsets adjacent to the initial offset target
CGFloat targetOffset = isHorizontal ? targetContentOffset->x : targetContentOffset->y;
#if RCT_SCROLL_SNAP_USE_KMP
targetOffset = [_sharedSnapOffsets resolveCurrentOffset:offsetAlongAxis
targetOffset:targetOffset
maximumOffset:maximumOffset
velocity:velocityAlongAxis
snapToStart:self.snapToStart
snapToEnd:self.snapToEnd]
.targetOffset;
#else
CGFloat smallerOffset = 0.0;
CGFloat largerOffset = maximumOffset;

Expand Down Expand Up @@ -250,6 +288,7 @@ - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView

// Make sure the new offset isn't out of bounds
targetOffset = MIN(MAX(0, targetOffset), maximumOffset);
#endif

// Set new targetContentOffset
if (isHorizontal) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import com.facebook.react.R
import com.facebook.react.common.ReactConstants
import com.facebook.react.common.build.ReactBuildConfig
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
import com.facebook.react.shared.ScrollSnapDirection
import com.facebook.react.shared.ScrollSnapOffsets
import com.facebook.react.uimanager.BackgroundStyleApplicator
import com.facebook.react.uimanager.HasChildPressedStateDelay
import com.facebook.react.uimanager.LengthPercentage
Expand Down Expand Up @@ -169,6 +171,7 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) :
private var disableIntervalMomentum = false
private var snapInterval = 0
private var snapOffsets: List<Int>? = null
private var sharedSnapOffsets: ScrollSnapOffsets? = null
private var snapToStart = true
private var snapToEnd = true
private var snapToAlignment = SNAP_ALIGNMENT_DISABLED
Expand Down Expand Up @@ -277,6 +280,7 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) :
disableIntervalMomentum = false
snapInterval = 0
snapOffsets = null
sharedSnapOffsets = null
snapToStart = true
snapToEnd = true
snapToAlignment = SNAP_ALIGNMENT_DISABLED
Expand Down Expand Up @@ -388,6 +392,7 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) :

public open fun setSnapOffsets(snapOffsets: List<Int>?) {
this.snapOffsets = snapOffsets
sharedSnapOffsets = snapOffsets?.let { ScrollSnapOffsets.fromIntegerOffsets(it) }
}

public open fun setSnapToStart(snapToStart: Boolean) {
Expand Down Expand Up @@ -1223,8 +1228,8 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) :

var smallerOffset = 0
var largerOffset = maximumOffset
var firstOffset = 0
var lastOffset = maximumOffset
val firstOffset = 0
val lastOffset = maximumOffset
val viewportWidth = width - paddingStart - paddingEnd

// offsets are from the right edge in RTL layouts
Expand All @@ -1233,26 +1238,21 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) :
velocityX = -velocityX
}

// get the nearest snap points to the target offset
val offsets = snapOffsets
if (!offsets.isNullOrEmpty()) {
firstOffset = offsets[0]
lastOffset = offsets[offsets.size - 1]

for (i in offsets.indices) {
val offset = offsets[i]
if (offset <= targetOffset) {
if (targetOffset - offset < targetOffset - smallerOffset) {
smallerOffset = offset
}
}
if (offset >= targetOffset) {
if (offset - targetOffset < largerOffset - targetOffset) {
largerOffset = offset
}
}
}
} else if (snapToAlignment != SNAP_ALIGNMENT_DISABLED) {
val snapResult =
sharedSnapOffsets
?.takeIf { !snapOffsets.isNullOrEmpty() }
?.resolve(
currentOffset =
(if (layoutDirection == LAYOUT_DIRECTION_RTL) maximumOffset - scrollX
else scrollX)
.toDouble(),
targetOffset = targetOffset.toDouble(),
maximumOffset = maximumOffset.toDouble(),
velocity = velocityX.toDouble(),
snapToStart = snapToStart,
snapToEnd = snapToEnd,
)
if (snapResult == null && snapToAlignment != SNAP_ALIGNMENT_DISABLED) {
if (snapInterval > 0) {
val ratio = targetOffset.toDouble() / snapInterval
smallerOffset =
Expand Down Expand Up @@ -1303,7 +1303,7 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) :
smallerOffset = max(smallerOffset, smallerChildOffset)
largerOffset = kotlin.math.min(largerOffset, largerChildOffset)
}
} else {
} else if (snapResult == null) {
val interval = getSnapInterval().toDouble()
val ratio = targetOffset.toDouble() / interval
smallerOffset = (floor(ratio) * interval).toInt()
Expand All @@ -1321,7 +1321,18 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) :
if (layoutDirection == LAYOUT_DIRECTION_RTL) {
currentOffset = maximumOffset - currentOffset
}
if (!snapToEnd && targetOffset >= lastOffset) {
if (snapResult != null) {
if (!hasCustomizedFlingAnimator) {
when (snapResult.direction) {
ScrollSnapDirection.FORWARD ->
velocityX += ((snapResult.selectedOffset.toInt() - targetOffset) * 10.0).toInt()
ScrollSnapDirection.BACKWARD ->
velocityX -= ((targetOffset - snapResult.selectedOffset.toInt()) * 10.0).toInt()
ScrollSnapDirection.NONE -> {}
}
}
targetOffset = snapResult.targetOffset.toInt()
} else if (!snapToEnd && targetOffset >= lastOffset) {
if (currentOffset >= lastOffset) {
// free scrolling
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import com.facebook.react.R
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.common.ReactConstants
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
import com.facebook.react.shared.ScrollSnapDirection
import com.facebook.react.shared.ScrollSnapOffsets
import com.facebook.react.uimanager.BackgroundStyleApplicator
import com.facebook.react.uimanager.HasChildPressedStateDelay
import com.facebook.react.uimanager.LengthPercentage
Expand Down Expand Up @@ -203,6 +205,7 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) :
private var disableIntervalMomentum = false
private var snapInterval = 0
private var snapOffsets: List<Int>? = null
private var sharedSnapOffsets: ScrollSnapOffsets? = null
private var snapToStart = true
private var snapToEnd = true
private var snapToAlignment = SNAP_ALIGNMENT_DISABLED
Expand Down Expand Up @@ -252,6 +255,7 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) :
disableIntervalMomentum = false
snapInterval = 0
snapOffsets = null
sharedSnapOffsets = null
snapToStart = true
snapToEnd = true
snapToAlignment = SNAP_ALIGNMENT_DISABLED
Expand Down Expand Up @@ -359,6 +363,7 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) :

open fun setSnapOffsets(snapOffsets: List<Int>?) {
this.snapOffsets = snapOffsets
sharedSnapOffsets = snapOffsets?.let { ScrollSnapOffsets.fromIntegerOffsets(it) }
}

open fun setSnapToStart(snapToStart: Boolean) {
Expand Down Expand Up @@ -923,29 +928,20 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) :

var smallerOffset = 0
var largerOffset = maximumOffset
var firstOffset = 0
var lastOffset = maximumOffset
val firstOffset = 0
val lastOffset = maximumOffset
val viewportHeight = height - paddingBottom - paddingTop

val currentSnapOffsets = snapOffsets
if (currentSnapOffsets != null) {
firstOffset = currentSnapOffsets[0]
lastOffset = currentSnapOffsets[currentSnapOffsets.size - 1]

for (i in currentSnapOffsets.indices) {
val offset = currentSnapOffsets[i]
if (offset <= targetOffset) {
if (targetOffset - offset < targetOffset - smallerOffset) {
smallerOffset = offset
}
}
if (offset >= targetOffset) {
if (offset - targetOffset < largerOffset - targetOffset) {
largerOffset = offset
}
}
}
} else if (snapToAlignment != SNAP_ALIGNMENT_DISABLED) {
val snapResult =
sharedSnapOffsets?.resolve(
currentOffset = scrollY.toDouble(),
targetOffset = targetOffset.toDouble(),
maximumOffset = maximumOffset.toDouble(),
velocity = velocityY.toDouble(),
snapToStart = snapToStart,
snapToEnd = snapToEnd,
)
if (snapResult == null && snapToAlignment != SNAP_ALIGNMENT_DISABLED) {
if (snapInterval > 0) {
val ratio = targetOffset.toDouble() / snapInterval
smallerOffset =
Expand Down Expand Up @@ -998,7 +994,7 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) :
smallerOffset = max(smallerOffset, smallerChildOffset)
largerOffset = min(largerOffset, largerChildOffset)
}
} else {
} else if (snapResult == null) {
val interval = getSnapInterval().toDouble()
val ratio = targetOffset.toDouble() / interval
smallerOffset = (floor(ratio) * interval).toInt()
Expand All @@ -1009,7 +1005,18 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) :
if (abs(targetOffset - smallerOffset) < abs(largerOffset - targetOffset)) smallerOffset
else largerOffset

if (!snapToEnd && targetOffset >= lastOffset) {
if (snapResult != null) {
if (!hasCustomizedFlingAnimator) {
when (snapResult.direction) {
ScrollSnapDirection.FORWARD ->
velocityY += ((snapResult.selectedOffset.toInt() - targetOffset) * 10.0).toInt()
ScrollSnapDirection.BACKWARD ->
velocityY -= ((targetOffset - snapResult.selectedOffset.toInt()) * 10.0).toInt()
ScrollSnapDirection.NONE -> {}
}
}
targetOffset = snapResult.targetOffset.toInt()
} else if (!snapToEnd && targetOffset >= lastOffset) {
if (scrollY >= lastOffset) {
// free scrolling
} else {
Expand Down
Loading
Loading