react-native-capped-scrollview 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CappedScrollView.podspec +20 -0
- package/LICENSE +20 -0
- package/README.md +125 -0
- package/android/build.gradle +67 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/cappedscrollview/CappedScrollViewModule.kt +205 -0
- package/android/src/main/java/com/cappedscrollview/CappedScrollViewPackage.kt +25 -0
- package/ios/CappedScrollView.h +9 -0
- package/ios/CappedScrollView.mm +160 -0
- package/lib/module/CappedScrollView.js +30 -0
- package/lib/module/CappedScrollView.js.map +1 -0
- package/lib/module/NativeCappedScrollView.js +5 -0
- package/lib/module/NativeCappedScrollView.js.map +1 -0
- package/lib/module/index.js +4 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/CappedScrollView.d.ts +248 -0
- package/lib/typescript/src/CappedScrollView.d.ts.map +1 -0
- package/lib/typescript/src/NativeCappedScrollView.d.ts +7 -0
- package/lib/typescript/src/NativeCappedScrollView.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +3 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/package.json +188 -0
- package/src/CappedScrollView.tsx +57 -0
- package/src/NativeCappedScrollView.ts +8 -0
- package/src/index.tsx +5 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "CappedScrollView"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"]
|
|
9
|
+
s.homepage = package["homepage"]
|
|
10
|
+
s.license = package["license"]
|
|
11
|
+
s.authors = package["author"]
|
|
12
|
+
|
|
13
|
+
s.platforms = { :ios => min_ios_version_supported }
|
|
14
|
+
s.source = { :git => "https://github.com/leonsilicon/react-native-capped-scrollview.git", :tag => "#{s.version}" }
|
|
15
|
+
|
|
16
|
+
s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
|
|
17
|
+
s.private_header_files = "ios/**/*.h"
|
|
18
|
+
|
|
19
|
+
install_modules_dependencies(s)
|
|
20
|
+
end
|
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Leon Si
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# react-native-capped-scrollview
|
|
2
|
+
|
|
3
|
+
A drop-in replacement for React Native's `ScrollView` that lets you cap how
|
|
4
|
+
fast the user can fling the list. Useful for surfaces where you want to keep
|
|
5
|
+
the visual scroll speed inside a comfortable, readable range — long-form
|
|
6
|
+
content, kiosk-style UIs, accessibility modes — without disabling momentum
|
|
7
|
+
entirely.
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<img src="https://raw.githubusercontent.com/leonsilicon/react-native-capped-scrollview/main/scrollview-capped.gif" width="320" alt="Side-by-side demo: capped column on the left flings at a controlled speed while the plain ScrollView on the right coasts naturally." />
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm install react-native-capped-scrollview
|
|
17
|
+
# or
|
|
18
|
+
yarn add react-native-capped-scrollview
|
|
19
|
+
# or
|
|
20
|
+
pnpm add react-native-capped-scrollview
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Then on iOS run `pod install` inside the `ios/` directory (or `npx expo prebuild` if you're on Expo). Android picks the library up automatically via autolinking.
|
|
24
|
+
|
|
25
|
+
Requires the New Architecture (Fabric + TurboModules). Tested against React Native 0.83; should work on any RN ≥ 0.80.
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import { CappedScrollView } from 'react-native-capped-scrollview';
|
|
31
|
+
|
|
32
|
+
export function MyList() {
|
|
33
|
+
return (
|
|
34
|
+
<CappedScrollView maxVelocity={0.25} style={{ flex: 1 }}>
|
|
35
|
+
{items.map((item) => (
|
|
36
|
+
<Row key={item.id} item={item} />
|
|
37
|
+
))}
|
|
38
|
+
</CappedScrollView>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`CappedScrollView` accepts every prop the built-in `ScrollView` accepts (it renders one under the hood) and adds a single new prop, `maxVelocity`.
|
|
44
|
+
|
|
45
|
+
The GIF above is the example app from `example/` — a `CappedScrollView` on the left and a plain `ScrollView` on the right being flung with the same gesture. Run it with `yarn example ios` or `yarn example android`.
|
|
46
|
+
|
|
47
|
+
## API
|
|
48
|
+
|
|
49
|
+
### `maxVelocity?: number | null`
|
|
50
|
+
|
|
51
|
+
Normalized fling-velocity cap.
|
|
52
|
+
|
|
53
|
+
| Value | Behaviour |
|
|
54
|
+
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
55
|
+
| `null`/omitted | No capper installed. Behaves exactly like a plain `ScrollView`. **Default.** |
|
|
56
|
+
| `0` | List cannot fling. Drag still works; releasing the finger stops the scroll dead. |
|
|
57
|
+
| `1` | Capper installed but set to the reference maximum. Human-paced flings pass through untouched; only faster-than-human programmatic flings get clamped. |
|
|
58
|
+
| `0 < x < 1` | Peak fling velocity is clamped to `x × 8000 dp/s` on Android (`x × 8000 pt/s` on iOS). The two units are identical, so the cap on each platform corresponds to the same logical scroll speed. |
|
|
59
|
+
|
|
60
|
+
The reference max is **8000 dp/s** on Android (scaled to physical pixels via `displayMetrics.density`) and **8000 pt/s** on iOS. Because a `dp` and a `pt` are the same logical unit, the same `maxVelocity` value applies the same peak-velocity cap on both platforms.
|
|
61
|
+
|
|
62
|
+
> [!NOTE]
|
|
63
|
+
> The cap controls *peak velocity*, not fling distance. iOS's `UIScrollView` decelerates exponentially while Android's `OverScroller` uses a spline whose distance is roughly `velocity^1.7`, so a single `maxVelocity` value produces a similar starting speed on both platforms but Android's fling will cover proportionally less distance than iOS's. If you need pixel-for-pixel cross-platform parity, drive the scroll yourself via `scrollTo` with an animation library.
|
|
64
|
+
|
|
65
|
+
The distinction between `null` and `1` is intentional: at `1` the cap mechanism is wired up (so future runtime changes via state are instant), whereas `null` skips the install entirely (the scroll view is byte-for-byte identical to RN's default).
|
|
66
|
+
|
|
67
|
+
### Ref
|
|
68
|
+
|
|
69
|
+
The ref returned by `CappedScrollView` is the underlying RN `ScrollView` instance — so `scrollTo`, `scrollToEnd`, `getScrollableNode`, etc. all work as you'd expect.
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
const ref = useRef<CappedScrollViewRef>(null);
|
|
73
|
+
// ...
|
|
74
|
+
ref.current?.scrollTo({ y: 0, animated: true });
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## How it works
|
|
78
|
+
|
|
79
|
+
The library never re-implements `ScrollView`. It wraps RN's own `<ScrollView>` (so every prop, every event, sticky headers, refresh control, etc. continue to work) and attaches a small native hook to the underlying platform scroll view to intercept fling velocity.
|
|
80
|
+
|
|
81
|
+
### iOS
|
|
82
|
+
|
|
83
|
+
A TurboModule resolves the React tag (`getScrollableNode()`) to the underlying `RCTScrollViewComponentView` by walking the window hierarchy and matching on `UIView.tag`, which Fabric sets to the React tag at mount time. Resolution retries every ~33 ms for up to ~1 s, because in Fabric the scroll view's UIView may not exist at the moment `useEffect` runs.
|
|
84
|
+
|
|
85
|
+
Once found, the module attaches a `UIScrollViewDelegate` proxy to the scroll view's `scrollViewDelegateSplitter` — RN exposes this as the official extension point for adding delegates without replacing the primary one, so all of RN's own scroll event plumbing keeps working.
|
|
86
|
+
|
|
87
|
+
The proxy implements `scrollViewWillEndDragging:withVelocity:targetContentOffset:`. UIScrollView reports velocity in points-per-millisecond and accepts an `inout` target offset. The proxy:
|
|
88
|
+
|
|
89
|
+
1. Computes `cap = fraction × 8000 pt/s` (converted to pt/ms to match UIScrollView's units).
|
|
90
|
+
2. If the user's fling is already within `cap`, leaves the target untouched.
|
|
91
|
+
3. Otherwise multiplies `(target − current)` by `cap / peak`, which produces a fling whose peak velocity is exactly `cap` and whose distance is linearly proportional to how much the velocity was clamped.
|
|
92
|
+
|
|
93
|
+
When `maxVelocity` becomes `null`, the proxy is removed via `[splitter removeDelegate:]` — the scroll view returns to its exact default state.
|
|
94
|
+
|
|
95
|
+
### Android
|
|
96
|
+
|
|
97
|
+
A TurboModule resolves the React tag via `UIManagerHelper.getUIManagerForReactTag(...).resolveView(tag)` (with a Fabric UIManager fallback), retrying on the main thread for up to ~1 s for the same Fabric mount-timing reason.
|
|
98
|
+
|
|
99
|
+
Once the `ReactScrollView` is found, the module replaces its `OverScroller` (Android's deceleration physics object — `mScroller` on the framework `ScrollView` class, plus RN's cached reference on `ReactScrollView`) with a subclass that overrides both `fling(...)` signatures. Both fields are written via reflection because they're declared `private`.
|
|
100
|
+
|
|
101
|
+
The replacement scroller:
|
|
102
|
+
|
|
103
|
+
1. Computes `cap = fraction × 8000 dp/s × displayMetrics.density` (matching the iOS reference of 8000 pt/s — same numeric value, same logical unit).
|
|
104
|
+
2. If the requested fling already fits the cap, forwards the fling unmodified.
|
|
105
|
+
3. Otherwise scales both `velocityX` and `velocityY` by `cap / peak` and calls `super.fling(...)`. The framework's deceleration physics then run from the clamped start velocity, producing a fling whose peak velocity is exactly `cap`. Distance is governed by `OverScroller`'s spline (≈ `velocity^1.7`), so fast flings end up coasting somewhat less than the equivalent iOS fling.
|
|
106
|
+
|
|
107
|
+
When `maxVelocity` becomes `null`, the module restores the original `OverScroller` it captured at install time, so the scroll view returns to its exact default state.
|
|
108
|
+
|
|
109
|
+
### JS glue
|
|
110
|
+
|
|
111
|
+
`src/CappedScrollView.tsx` is a `forwardRef` wrapper around `<ScrollView>`. On `maxVelocity` change, it calls `innerRef.current?.getScrollableNode()` to get the native React tag and forwards `(tag, value)` to the TurboModule. `null` is encoded as the sentinel `-1`, which the native side interprets as "remove the cap from this scroll view."
|
|
112
|
+
|
|
113
|
+
## Contributing
|
|
114
|
+
|
|
115
|
+
- [Development workflow](CONTRIBUTING.md#development-workflow)
|
|
116
|
+
- [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
|
|
117
|
+
- [Code of conduct](CODE_OF_CONDUCT.md)
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
MIT
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.CappedScrollview = [
|
|
3
|
+
kotlinVersion: "2.0.21",
|
|
4
|
+
minSdkVersion: 24,
|
|
5
|
+
compileSdkVersion: 36,
|
|
6
|
+
targetSdkVersion: 36
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
ext.getExtOrDefault = { prop ->
|
|
10
|
+
if (rootProject.ext.has(prop)) {
|
|
11
|
+
return rootProject.ext.get(prop)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return CappedScrollview[prop]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
repositories {
|
|
18
|
+
google()
|
|
19
|
+
mavenCentral()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
dependencies {
|
|
23
|
+
classpath "com.android.tools.build:gradle:8.7.2"
|
|
24
|
+
// noinspection DifferentKotlinGradleVersion
|
|
25
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
apply plugin: "com.android.library"
|
|
31
|
+
apply plugin: "kotlin-android"
|
|
32
|
+
|
|
33
|
+
apply plugin: "com.facebook.react"
|
|
34
|
+
|
|
35
|
+
android {
|
|
36
|
+
namespace "com.cappedscrollview"
|
|
37
|
+
|
|
38
|
+
compileSdkVersion getExtOrDefault("compileSdkVersion")
|
|
39
|
+
|
|
40
|
+
defaultConfig {
|
|
41
|
+
minSdkVersion getExtOrDefault("minSdkVersion")
|
|
42
|
+
targetSdkVersion getExtOrDefault("targetSdkVersion")
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
buildFeatures {
|
|
46
|
+
buildConfig true
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
buildTypes {
|
|
50
|
+
release {
|
|
51
|
+
minifyEnabled false
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
lint {
|
|
56
|
+
disable "GradleCompatible"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
compileOptions {
|
|
60
|
+
sourceCompatibility JavaVersion.VERSION_1_8
|
|
61
|
+
targetCompatibility JavaVersion.VERSION_1_8
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
dependencies {
|
|
66
|
+
implementation "com.facebook.react:react-android"
|
|
67
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
package com.cappedscrollview
|
|
2
|
+
|
|
3
|
+
import android.os.Handler
|
|
4
|
+
import android.os.Looper
|
|
5
|
+
import android.widget.OverScroller
|
|
6
|
+
import android.widget.ScrollView
|
|
7
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
8
|
+
import com.facebook.react.bridge.ReactContext
|
|
9
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
10
|
+
import com.facebook.react.uimanager.UIManagerHelper
|
|
11
|
+
import com.facebook.react.uimanager.common.UIManagerType
|
|
12
|
+
import com.facebook.react.views.scroll.ReactScrollView
|
|
13
|
+
import java.lang.reflect.Field
|
|
14
|
+
import kotlin.math.abs
|
|
15
|
+
|
|
16
|
+
@ReactModule(name = CappedScrollViewModule.NAME)
|
|
17
|
+
class CappedScrollViewModule(reactContext: ReactApplicationContext) :
|
|
18
|
+
NativeCappedScrollViewSpec(reactContext) {
|
|
19
|
+
|
|
20
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
21
|
+
|
|
22
|
+
override fun getName(): String = NAME
|
|
23
|
+
|
|
24
|
+
override fun setMaxVelocity(reactTag: Double, maxVelocity: Double) {
|
|
25
|
+
val tag = reactTag.toInt()
|
|
26
|
+
attemptInstall(tag, maxVelocity, attemptsRemaining = MAX_RESOLVE_ATTEMPTS)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private fun attemptInstall(tag: Int, value: Double, attemptsRemaining: Int) {
|
|
30
|
+
mainHandler.post {
|
|
31
|
+
val view = resolveView(reactApplicationContext, tag)
|
|
32
|
+
if (view !is ReactScrollView) {
|
|
33
|
+
if (attemptsRemaining > 0) {
|
|
34
|
+
mainHandler.postDelayed({
|
|
35
|
+
attemptInstall(tag, value, attemptsRemaining - 1)
|
|
36
|
+
}, RETRY_DELAY_MS)
|
|
37
|
+
}
|
|
38
|
+
return@post
|
|
39
|
+
}
|
|
40
|
+
if (value < 0) {
|
|
41
|
+
uninstallCap(view)
|
|
42
|
+
} else {
|
|
43
|
+
installCap(view, value)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private fun uninstallCap(view: ReactScrollView) {
|
|
49
|
+
val baseField = scrollViewScrollerField() ?: return
|
|
50
|
+
val current = try {
|
|
51
|
+
baseField.get(view) as? OverScroller
|
|
52
|
+
} catch (_: Throwable) {
|
|
53
|
+
null
|
|
54
|
+
}
|
|
55
|
+
val original = (current as? CappedOverScroller)?.original ?: return
|
|
56
|
+
try {
|
|
57
|
+
baseField.set(view, original)
|
|
58
|
+
} catch (_: Throwable) {
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
reactScrollViewScrollerField()?.set(view, original)
|
|
63
|
+
} catch (_: Throwable) {
|
|
64
|
+
// Best-effort.
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private fun resolveView(context: ReactContext, tag: Int): android.view.View? {
|
|
69
|
+
val byTag = try {
|
|
70
|
+
UIManagerHelper.getUIManagerForReactTag(context, tag)?.resolveView(tag)
|
|
71
|
+
} catch (_: Throwable) {
|
|
72
|
+
null
|
|
73
|
+
}
|
|
74
|
+
if (byTag != null) return byTag
|
|
75
|
+
return try {
|
|
76
|
+
UIManagerHelper.getUIManager(context, UIManagerType.FABRIC)?.resolveView(tag)
|
|
77
|
+
} catch (_: Throwable) {
|
|
78
|
+
null
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private fun installCap(view: ReactScrollView, fraction: Double) {
|
|
83
|
+
val baseField = scrollViewScrollerField() ?: return
|
|
84
|
+
val rnField = reactScrollViewScrollerField()
|
|
85
|
+
val current = try {
|
|
86
|
+
baseField.get(view) as? OverScroller
|
|
87
|
+
} catch (_: Throwable) {
|
|
88
|
+
null
|
|
89
|
+
}
|
|
90
|
+
if (current is CappedOverScroller) {
|
|
91
|
+
current.fraction = fraction
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
// The cap reference is 8000 dp/s — the same numeric value used on iOS,
|
|
95
|
+
// expressed in dp/pt so the two platforms produce the same scroll feel
|
|
96
|
+
// at the same `maxVelocity` value. Convert to physical pixels for the
|
|
97
|
+
// OverScroller (which accepts px/s).
|
|
98
|
+
val density = view.resources.displayMetrics.density
|
|
99
|
+
val referenceMaxPxPerSec = REFERENCE_MAX_DP_PER_SEC * density
|
|
100
|
+
val replacement = CappedOverScroller(
|
|
101
|
+
view.context,
|
|
102
|
+
fraction,
|
|
103
|
+
referenceMaxPxPerSec.toDouble(),
|
|
104
|
+
current,
|
|
105
|
+
)
|
|
106
|
+
try {
|
|
107
|
+
baseField.set(view, replacement)
|
|
108
|
+
} catch (_: Throwable) {
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
rnField?.set(view, replacement)
|
|
113
|
+
} catch (_: Throwable) {
|
|
114
|
+
// Best-effort; RN's cached reference will fall back to base ScrollView behavior.
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private fun scrollViewScrollerField(): Field? = scrollerFieldCache ?: run {
|
|
119
|
+
try {
|
|
120
|
+
val field = ScrollView::class.java.getDeclaredField("mScroller")
|
|
121
|
+
field.isAccessible = true
|
|
122
|
+
scrollerFieldCache = field
|
|
123
|
+
field
|
|
124
|
+
} catch (_: Throwable) {
|
|
125
|
+
null
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private fun reactScrollViewScrollerField(): Field? = rnScrollerFieldCache ?: run {
|
|
130
|
+
try {
|
|
131
|
+
val field = ReactScrollView::class.java.getDeclaredField("mScroller")
|
|
132
|
+
field.isAccessible = true
|
|
133
|
+
rnScrollerFieldCache = field
|
|
134
|
+
field
|
|
135
|
+
} catch (_: NoSuchFieldException) {
|
|
136
|
+
null
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
companion object {
|
|
141
|
+
const val NAME = "CappedScrollView"
|
|
142
|
+
private const val RETRY_DELAY_MS = 33L
|
|
143
|
+
private const val MAX_RESOLVE_ATTEMPTS = 30
|
|
144
|
+
// Public 0..1 cap scale is anchored to 8000 dp/s (matches iOS's 8000 pt/s).
|
|
145
|
+
private const val REFERENCE_MAX_DP_PER_SEC = 8000f
|
|
146
|
+
@Volatile private var scrollerFieldCache: Field? = null
|
|
147
|
+
@Volatile private var rnScrollerFieldCache: Field? = null
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* OverScroller subclass that caps peak fling velocity at
|
|
153
|
+
* `referenceMax * fraction` (where `referenceMax` is 8000 dp/s converted to
|
|
154
|
+
* physical pixels, matching iOS's 8000 pt/s reference). The base RN/Android
|
|
155
|
+
* deceleration physics are otherwise left alone, so the resulting fling
|
|
156
|
+
* decelerates naturally from the clamped peak.
|
|
157
|
+
*/
|
|
158
|
+
private class CappedOverScroller(
|
|
159
|
+
context: android.content.Context,
|
|
160
|
+
@Volatile var fraction: Double,
|
|
161
|
+
private val referenceMaxPxPerSec: Double,
|
|
162
|
+
val original: OverScroller?,
|
|
163
|
+
) : OverScroller(context) {
|
|
164
|
+
|
|
165
|
+
override fun fling(
|
|
166
|
+
startX: Int,
|
|
167
|
+
startY: Int,
|
|
168
|
+
velocityX: Int,
|
|
169
|
+
velocityY: Int,
|
|
170
|
+
minX: Int,
|
|
171
|
+
maxX: Int,
|
|
172
|
+
minY: Int,
|
|
173
|
+
maxY: Int,
|
|
174
|
+
overX: Int,
|
|
175
|
+
overY: Int,
|
|
176
|
+
) {
|
|
177
|
+
val (cx, cy) = applyCap(velocityX, velocityY)
|
|
178
|
+
super.fling(startX, startY, cx, cy, minX, maxX, minY, maxY, overX, overY)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
override fun fling(
|
|
182
|
+
startX: Int,
|
|
183
|
+
startY: Int,
|
|
184
|
+
velocityX: Int,
|
|
185
|
+
velocityY: Int,
|
|
186
|
+
minX: Int,
|
|
187
|
+
maxX: Int,
|
|
188
|
+
minY: Int,
|
|
189
|
+
maxY: Int,
|
|
190
|
+
) {
|
|
191
|
+
val (cx, cy) = applyCap(velocityX, velocityY)
|
|
192
|
+
super.fling(startX, startY, cx, cy, minX, maxX, minY, maxY)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private fun applyCap(velocityX: Int, velocityY: Int): Pair<Int, Int> {
|
|
196
|
+
val f = fraction.coerceIn(0.0, 1.0)
|
|
197
|
+
if (f >= 1.0) return velocityX to velocityY
|
|
198
|
+
if (f <= 0.0) return 0 to 0
|
|
199
|
+
val cap = referenceMaxPxPerSec * f
|
|
200
|
+
val peak = maxOf(abs(velocityX), abs(velocityY)).toDouble()
|
|
201
|
+
if (peak <= cap) return velocityX to velocityY
|
|
202
|
+
val scale = cap / peak
|
|
203
|
+
return (velocityX * scale).toInt() to (velocityY * scale).toInt()
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
package com.cappedscrollview
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.BaseReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfo
|
|
7
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
8
|
+
|
|
9
|
+
class CappedScrollViewPackage : BaseReactPackage() {
|
|
10
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
|
|
11
|
+
if (name == CappedScrollViewModule.NAME) CappedScrollViewModule(reactContext) else null
|
|
12
|
+
|
|
13
|
+
override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
|
|
14
|
+
mapOf(
|
|
15
|
+
CappedScrollViewModule.NAME to ReactModuleInfo(
|
|
16
|
+
CappedScrollViewModule.NAME,
|
|
17
|
+
CappedScrollViewModule::class.java.name,
|
|
18
|
+
false,
|
|
19
|
+
false,
|
|
20
|
+
false,
|
|
21
|
+
true,
|
|
22
|
+
)
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#import "CappedScrollView.h"
|
|
2
|
+
|
|
3
|
+
#import <React/RCTBridge.h>
|
|
4
|
+
#import <React/RCTBridgeModule.h>
|
|
5
|
+
#import <React/RCTScrollViewComponentView.h>
|
|
6
|
+
#import <React/RCTUtils.h>
|
|
7
|
+
|
|
8
|
+
@interface CappedScrollViewVelocityCapper : NSObject <UIScrollViewDelegate>
|
|
9
|
+
@property (nonatomic, assign) CGFloat maxVelocity;
|
|
10
|
+
@end
|
|
11
|
+
|
|
12
|
+
static RCTScrollViewComponentView *_Nullable CappedScrollViewSearch(UIView *root, NSInteger tag)
|
|
13
|
+
{
|
|
14
|
+
if (root.tag == tag && [root isKindOfClass:[RCTScrollViewComponentView class]]) {
|
|
15
|
+
return (RCTScrollViewComponentView *)root;
|
|
16
|
+
}
|
|
17
|
+
for (UIView *child in root.subviews) {
|
|
18
|
+
RCTScrollViewComponentView *match = CappedScrollViewSearch(child, tag);
|
|
19
|
+
if (match) {
|
|
20
|
+
return match;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return nil;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static RCTScrollViewComponentView *_Nullable CappedScrollViewFindScrollView(NSInteger tag)
|
|
27
|
+
{
|
|
28
|
+
for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) {
|
|
29
|
+
if (![scene isKindOfClass:[UIWindowScene class]]) continue;
|
|
30
|
+
for (UIWindow *window in ((UIWindowScene *)scene).windows) {
|
|
31
|
+
RCTScrollViewComponentView *match = CappedScrollViewSearch(window, tag);
|
|
32
|
+
if (match) {
|
|
33
|
+
return match;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return nil;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Cross-platform reference for the public 0..1 cap scale: 8000 pt/s on iOS,
|
|
41
|
+
// 8000 dp/s on Android. At maxVelocity=1 the cap exactly matches this value;
|
|
42
|
+
// at 0.5 it is 4000 pt/s, etc.
|
|
43
|
+
static const CGFloat kCappedScrollViewReferenceMaxPtsPerSec = 8000.0;
|
|
44
|
+
|
|
45
|
+
@implementation CappedScrollViewVelocityCapper
|
|
46
|
+
|
|
47
|
+
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView
|
|
48
|
+
withVelocity:(CGPoint)velocity
|
|
49
|
+
targetContentOffset:(inout CGPoint *)targetContentOffset
|
|
50
|
+
{
|
|
51
|
+
CGFloat fraction = MAX(0.0, MIN(1.0, self.maxVelocity));
|
|
52
|
+
if (fraction >= 1.0) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (fraction <= 0.0) {
|
|
56
|
+
*targetContentOffset = scrollView.contentOffset;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// UIScrollView reports velocity in points-per-millisecond.
|
|
61
|
+
CGFloat capPerMs = (kCappedScrollViewReferenceMaxPtsPerSec * fraction) / 1000.0;
|
|
62
|
+
CGFloat peak = MAX(fabs(velocity.x), fabs(velocity.y));
|
|
63
|
+
if (peak <= capPerMs) {
|
|
64
|
+
// Natural fling already within the cap — leave it untouched.
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Speed-limit model: scale the predicted target offset toward current by
|
|
69
|
+
// (cap / peak). Linear scaling here means the fling distance is exactly
|
|
70
|
+
// proportional to how much we've clamped peak velocity, matching the
|
|
71
|
+
// Android behaviour (which clamps `velocityY` to `cap` directly).
|
|
72
|
+
CGFloat scale = capPerMs / peak;
|
|
73
|
+
CGPoint current = scrollView.contentOffset;
|
|
74
|
+
CGPoint target = *targetContentOffset;
|
|
75
|
+
target.x = current.x + (target.x - current.x) * scale;
|
|
76
|
+
target.y = current.y + (target.y - current.y) * scale;
|
|
77
|
+
*targetContentOffset = target;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@end
|
|
81
|
+
|
|
82
|
+
@implementation CappedScrollView {
|
|
83
|
+
NSMapTable<NSNumber *, CappedScrollViewVelocityCapper *> *_cappersByTag;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
RCT_EXPORT_MODULE()
|
|
87
|
+
|
|
88
|
+
@synthesize bridge = _bridge;
|
|
89
|
+
@synthesize viewRegistry_DEPRECATED = _viewRegistry_DEPRECATED;
|
|
90
|
+
|
|
91
|
+
- (instancetype)init
|
|
92
|
+
{
|
|
93
|
+
if (self = [super init]) {
|
|
94
|
+
_cappersByTag = [NSMapTable strongToStrongObjectsMapTable];
|
|
95
|
+
}
|
|
96
|
+
return self;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
- (void)setMaxVelocity:(double)reactTag maxVelocity:(double)maxVelocity
|
|
100
|
+
{
|
|
101
|
+
NSInteger tag = (NSInteger)reactTag;
|
|
102
|
+
CGFloat cap = (CGFloat)maxVelocity;
|
|
103
|
+
__weak __typeof(self) weakSelf = self;
|
|
104
|
+
[self attemptInstallForTag:tag cap:cap attemptsRemaining:30 weakSelf:weakSelf];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
- (void)attemptInstallForTag:(NSInteger)tag
|
|
108
|
+
cap:(CGFloat)cap
|
|
109
|
+
attemptsRemaining:(NSInteger)attemptsRemaining
|
|
110
|
+
weakSelf:(__weak CappedScrollView *)weakSelf
|
|
111
|
+
{
|
|
112
|
+
RCTExecuteOnMainQueue(^{
|
|
113
|
+
__typeof(self) strongSelf = weakSelf;
|
|
114
|
+
if (!strongSelf) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
RCTScrollViewComponentView *scrollComponent = CappedScrollViewFindScrollView(tag);
|
|
119
|
+
if (!scrollComponent) {
|
|
120
|
+
if (attemptsRemaining > 0) {
|
|
121
|
+
// Fabric mounting can lag the JS-side ref attachment; retry up to ~1s.
|
|
122
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(33 * NSEC_PER_MSEC)),
|
|
123
|
+
dispatch_get_main_queue(), ^{
|
|
124
|
+
[strongSelf attemptInstallForTag:tag
|
|
125
|
+
cap:cap
|
|
126
|
+
attemptsRemaining:attemptsRemaining - 1
|
|
127
|
+
weakSelf:weakSelf];
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
NSNumber *key = @(tag);
|
|
134
|
+
CappedScrollViewVelocityCapper *capper = [strongSelf->_cappersByTag objectForKey:key];
|
|
135
|
+
|
|
136
|
+
if (cap < 0) {
|
|
137
|
+
// Sentinel: caller wants no capper at all.
|
|
138
|
+
if (capper) {
|
|
139
|
+
[scrollComponent.scrollViewDelegateSplitter removeDelegate:capper];
|
|
140
|
+
[strongSelf->_cappersByTag removeObjectForKey:key];
|
|
141
|
+
}
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!capper) {
|
|
146
|
+
capper = [CappedScrollViewVelocityCapper new];
|
|
147
|
+
[strongSelf->_cappersByTag setObject:capper forKey:key];
|
|
148
|
+
[scrollComponent.scrollViewDelegateSplitter addDelegate:capper];
|
|
149
|
+
}
|
|
150
|
+
capper.maxVelocity = cap;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
|
155
|
+
(const facebook::react::ObjCTurboModule::InitParams &)params
|
|
156
|
+
{
|
|
157
|
+
return std::make_shared<facebook::react::NativeCappedScrollViewSpecJSI>(params);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { forwardRef, useEffect, useRef, useImperativeHandle } from 'react';
|
|
4
|
+
import { ScrollView } from 'react-native';
|
|
5
|
+
import NativeCappedScrollView from "./NativeCappedScrollView.js";
|
|
6
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
7
|
+
// Sentinel sent to native when consumers pass `null`/omit the prop, meaning
|
|
8
|
+
// "no capper installed at all." Negative values are unreachable from the
|
|
9
|
+
// public [0, 1] range, so this is unambiguous on the native side.
|
|
10
|
+
const NO_CAP_SENTINEL = -1;
|
|
11
|
+
export const CappedScrollView = /*#__PURE__*/forwardRef(function CappedScrollView({
|
|
12
|
+
maxVelocity,
|
|
13
|
+
...rest
|
|
14
|
+
}, ref) {
|
|
15
|
+
const innerRef = useRef(null);
|
|
16
|
+
useImperativeHandle(ref, () => innerRef.current);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const handle = innerRef.current?.getScrollableNode?.();
|
|
19
|
+
if (handle == null) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const value = maxVelocity == null ? NO_CAP_SENTINEL : maxVelocity;
|
|
23
|
+
NativeCappedScrollView.setMaxVelocity(handle, value);
|
|
24
|
+
}, [maxVelocity]);
|
|
25
|
+
return /*#__PURE__*/_jsx(ScrollView, {
|
|
26
|
+
ref: innerRef,
|
|
27
|
+
...rest
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
//# sourceMappingURL=CappedScrollView.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["forwardRef","useEffect","useRef","useImperativeHandle","ScrollView","NativeCappedScrollView","jsx","_jsx","NO_CAP_SENTINEL","CappedScrollView","maxVelocity","rest","ref","innerRef","current","handle","getScrollableNode","value","setMaxVelocity"],"sourceRoot":"../../src","sources":["CappedScrollView.tsx"],"mappings":";;AAAA,SACEA,UAAU,EACVC,SAAS,EACTC,MAAM,EACNC,mBAAmB,QAGd,OAAO;AACd,SAASC,UAAU,QAA8B,cAAc;AAC/D,OAAOC,sBAAsB,MAAM,6BAA0B;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAwB9D;AACA;AACA;AACA,MAAMC,eAAe,GAAG,CAAC,CAAC;AAE1B,OAAO,MAAMC,gBAAgB,gBAAGT,UAAU,CAAC,SAASS,gBAAgBA,CAClE;EAAEC,WAAW;EAAE,GAAGC;AAA4B,CAAC,EAC/CC,GAA6B,EAC7B;EACA,MAAMC,QAAQ,GAAGX,MAAM,CAA6B,IAAI,CAAC;EAEzDC,mBAAmB,CAACS,GAAG,EAAE,MAAMC,QAAQ,CAACC,OAA8B,CAAC;EAEvEb,SAAS,CAAC,MAAM;IACd,MAAMc,MAAM,GAAGF,QAAQ,CAACC,OAAO,EAAEE,iBAAiB,GAAG,CAAC;IACtD,IAAID,MAAM,IAAI,IAAI,EAAE;MAClB;IACF;IACA,MAAME,KAAK,GAAGP,WAAW,IAAI,IAAI,GAAGF,eAAe,GAAGE,WAAW;IACjEL,sBAAsB,CAACa,cAAc,CAACH,MAAM,EAAEE,KAAK,CAAC;EACtD,CAAC,EAAE,CAACP,WAAW,CAAC,CAAC;EAEjB,oBAAOH,IAAA,CAACH,UAAU;IAACQ,GAAG,EAAEC,QAAS;IAAA,GAAKF;EAAI,CAAG,CAAC;AAChD,CAAC,CAAC","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeCappedScrollView.ts"],"mappings":";;AACA,SAASA,mBAAmB,QAAQ,cAAc;AAMlD,eAAeA,mBAAmB,CAACC,YAAY,CAAO,kBAAkB,CAAC","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["CappedScrollView"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,SAASA,gBAAgB,QAAQ,uBAAoB","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { type ComponentRef } from 'react';
|
|
2
|
+
import { ScrollView, type ScrollViewProps } from 'react-native';
|
|
3
|
+
export type CappedScrollViewRef = ComponentRef<typeof ScrollView>;
|
|
4
|
+
export type CappedScrollViewProps = ScrollViewProps & {
|
|
5
|
+
/**
|
|
6
|
+
* Fling velocity cap as a normalized fraction in [0, 1], or `null` to
|
|
7
|
+
* disable the cap mechanism entirely.
|
|
8
|
+
*
|
|
9
|
+
* The reference maximum is 8000 dp/s on Android and 8000 pt/s on iOS
|
|
10
|
+
* (same logical unit), so the same `maxVelocity` value produces the same
|
|
11
|
+
* visual scroll speed on both platforms.
|
|
12
|
+
*
|
|
13
|
+
* - `null` / `undefined` (default) — no capper installed; behaves exactly
|
|
14
|
+
* like a plain RN `ScrollView`.
|
|
15
|
+
* - `1` — capper installed at the reference maximum. Human-paced flings
|
|
16
|
+
* pass through untouched.
|
|
17
|
+
* - `0` — list cannot fling at all.
|
|
18
|
+
* - `0 < x < 1` — peak fling velocity is clamped to `x × 8000` (dp/s on
|
|
19
|
+
* Android, pt/s on iOS). Fling distance scales linearly with `x`.
|
|
20
|
+
*/
|
|
21
|
+
maxVelocity?: number | null;
|
|
22
|
+
};
|
|
23
|
+
export declare const CappedScrollView: import("react").ForwardRefExoticComponent<Readonly<Omit<Omit<Readonly<Omit<Readonly<{
|
|
24
|
+
onAccessibilityAction?: ((event: import("react-native").AccessibilityActionEvent) => unknown) | undefined;
|
|
25
|
+
onAccessibilityTap?: (() => unknown) | undefined;
|
|
26
|
+
onLayout?: ((event: import("react-native").LayoutChangeEvent) => unknown) | undefined;
|
|
27
|
+
onMagicTap?: (() => unknown) | undefined;
|
|
28
|
+
onAccessibilityEscape?: (() => unknown) | undefined;
|
|
29
|
+
}>, "onMoveShouldSetResponder" | "onMoveShouldSetResponderCapture" | "onResponderGrant" | "onResponderMove" | "onResponderReject" | "onResponderRelease" | "onResponderStart" | "onResponderEnd" | "onResponderTerminate" | "onResponderTerminationRequest" | "onStartShouldSetResponder" | "onStartShouldSetResponderCapture" | "onMouseEnter" | "onMouseLeave" | "onClick" | "onClickCapture" | "onPointerEnter" | "onPointerEnterCapture" | "onPointerLeave" | "onPointerLeaveCapture" | "onPointerMove" | "onPointerMoveCapture" | "onPointerCancel" | "onPointerCancelCapture" | "onPointerDown" | "onPointerDownCapture" | "onPointerUp" | "onPointerUpCapture" | "onPointerOver" | "onPointerOverCapture" | "onPointerOut" | "onPointerOutCapture" | "onGotPointerCapture" | "onGotPointerCaptureCapture" | "onLostPointerCapture" | "onLostPointerCaptureCapture" | "onBlur" | "onBlurCapture" | "onFocus" | "onFocusCapture" | "onTouchCancel" | "onTouchCancelCapture" | "onTouchEnd" | "onTouchEndCapture" | "onTouchMove" | "onTouchMoveCapture" | "onTouchStart" | "onTouchStartCapture" | "nativeBackgroundAndroid" | "nativeForegroundAndroid" | "renderToHardwareTextureAndroid" | "hasTVPreferredFocus" | "nextFocusDown" | "nextFocusForward" | "nextFocusLeft" | "nextFocusRight" | "nextFocusUp" | "focusable" | "tabIndex" | "shouldRasterizeIOS" | "accessibilityIgnoresInvertColors" | "accessibilityViewIsModal" | "accessibilityShowsLargeContentViewer" | "accessibilityLargeContentTitle" | "aria-modal" | "accessibilityElementsHidden" | "accessibilityLanguage" | "accessibilityRespondsToUserInteraction" | "accessible" | "accessibilityLabel" | "accessibilityHint" | "aria-label" | "accessibilityRole" | "role" | "accessibilityState" | "accessibilityValue" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-valuetext" | "accessibilityActions" | "aria-busy" | "aria-checked" | "aria-disabled" | "aria-expanded" | "aria-selected" | "aria-hidden" | "accessibilityLabelledBy" | "aria-labelledby" | "accessibilityLiveRegion" | "aria-live" | "importantForAccessibility" | "screenReaderFocusable" | "children" | "style" | "collapsable" | "collapsableChildren" | "id" | "testID" | "nativeID" | "needsOffscreenAlphaCompositing" | "hitSlop" | "pointerEvents" | "removeClippedSubviews" | "experimental_accessibilityOrder"> & Omit<Readonly<{
|
|
30
|
+
onMoveShouldSetResponder?: ((e: import("react-native").GestureResponderEvent) => boolean) | undefined;
|
|
31
|
+
onMoveShouldSetResponderCapture?: ((e: import("react-native").GestureResponderEvent) => boolean) | undefined;
|
|
32
|
+
onResponderGrant?: ((e: import("react-native").GestureResponderEvent) => void | boolean) | undefined;
|
|
33
|
+
onResponderMove?: ((e: import("react-native").GestureResponderEvent) => void) | undefined;
|
|
34
|
+
onResponderReject?: ((e: import("react-native").GestureResponderEvent) => void) | undefined;
|
|
35
|
+
onResponderRelease?: ((e: import("react-native").GestureResponderEvent) => void) | undefined;
|
|
36
|
+
onResponderStart?: ((e: import("react-native").GestureResponderEvent) => void) | undefined;
|
|
37
|
+
onResponderEnd?: ((e: import("react-native").GestureResponderEvent) => void) | undefined;
|
|
38
|
+
onResponderTerminate?: ((e: import("react-native").GestureResponderEvent) => void) | undefined;
|
|
39
|
+
onResponderTerminationRequest?: ((e: import("react-native").GestureResponderEvent) => boolean) | undefined;
|
|
40
|
+
onStartShouldSetResponder?: ((e: import("react-native").GestureResponderEvent) => boolean) | undefined;
|
|
41
|
+
onStartShouldSetResponderCapture?: ((e: import("react-native").GestureResponderEvent) => boolean) | undefined;
|
|
42
|
+
}>, "onMouseEnter" | "onMouseLeave" | "onClick" | "onClickCapture" | "onPointerEnter" | "onPointerEnterCapture" | "onPointerLeave" | "onPointerLeaveCapture" | "onPointerMove" | "onPointerMoveCapture" | "onPointerCancel" | "onPointerCancelCapture" | "onPointerDown" | "onPointerDownCapture" | "onPointerUp" | "onPointerUpCapture" | "onPointerOver" | "onPointerOverCapture" | "onPointerOut" | "onPointerOutCapture" | "onGotPointerCapture" | "onGotPointerCaptureCapture" | "onLostPointerCapture" | "onLostPointerCaptureCapture" | "onBlur" | "onBlurCapture" | "onFocus" | "onFocusCapture" | "onTouchCancel" | "onTouchCancelCapture" | "onTouchEnd" | "onTouchEndCapture" | "onTouchMove" | "onTouchMoveCapture" | "onTouchStart" | "onTouchStartCapture" | "nativeBackgroundAndroid" | "nativeForegroundAndroid" | "renderToHardwareTextureAndroid" | "hasTVPreferredFocus" | "nextFocusDown" | "nextFocusForward" | "nextFocusLeft" | "nextFocusRight" | "nextFocusUp" | "focusable" | "tabIndex" | "shouldRasterizeIOS" | "accessibilityIgnoresInvertColors" | "accessibilityViewIsModal" | "accessibilityShowsLargeContentViewer" | "accessibilityLargeContentTitle" | "aria-modal" | "accessibilityElementsHidden" | "accessibilityLanguage" | "accessibilityRespondsToUserInteraction" | "accessible" | "accessibilityLabel" | "accessibilityHint" | "aria-label" | "accessibilityRole" | "role" | "accessibilityState" | "accessibilityValue" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-valuetext" | "accessibilityActions" | "aria-busy" | "aria-checked" | "aria-disabled" | "aria-expanded" | "aria-selected" | "aria-hidden" | "accessibilityLabelledBy" | "aria-labelledby" | "accessibilityLiveRegion" | "aria-live" | "importantForAccessibility" | "screenReaderFocusable" | "children" | "style" | "collapsable" | "collapsableChildren" | "id" | "testID" | "nativeID" | "needsOffscreenAlphaCompositing" | "hitSlop" | "pointerEvents" | "removeClippedSubviews" | "experimental_accessibilityOrder"> & Omit<Readonly<{
|
|
43
|
+
onMouseEnter?: ((event: import("react-native").MouseEvent) => void) | undefined;
|
|
44
|
+
onMouseLeave?: ((event: import("react-native").MouseEvent) => void) | undefined;
|
|
45
|
+
}>, "onClick" | "onClickCapture" | "onPointerEnter" | "onPointerEnterCapture" | "onPointerLeave" | "onPointerLeaveCapture" | "onPointerMove" | "onPointerMoveCapture" | "onPointerCancel" | "onPointerCancelCapture" | "onPointerDown" | "onPointerDownCapture" | "onPointerUp" | "onPointerUpCapture" | "onPointerOver" | "onPointerOverCapture" | "onPointerOut" | "onPointerOutCapture" | "onGotPointerCapture" | "onGotPointerCaptureCapture" | "onLostPointerCapture" | "onLostPointerCaptureCapture" | "onBlur" | "onBlurCapture" | "onFocus" | "onFocusCapture" | "onTouchCancel" | "onTouchCancelCapture" | "onTouchEnd" | "onTouchEndCapture" | "onTouchMove" | "onTouchMoveCapture" | "onTouchStart" | "onTouchStartCapture" | "nativeBackgroundAndroid" | "nativeForegroundAndroid" | "renderToHardwareTextureAndroid" | "hasTVPreferredFocus" | "nextFocusDown" | "nextFocusForward" | "nextFocusLeft" | "nextFocusRight" | "nextFocusUp" | "focusable" | "tabIndex" | "shouldRasterizeIOS" | "accessibilityIgnoresInvertColors" | "accessibilityViewIsModal" | "accessibilityShowsLargeContentViewer" | "accessibilityLargeContentTitle" | "aria-modal" | "accessibilityElementsHidden" | "accessibilityLanguage" | "accessibilityRespondsToUserInteraction" | "accessible" | "accessibilityLabel" | "accessibilityHint" | "aria-label" | "accessibilityRole" | "role" | "accessibilityState" | "accessibilityValue" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-valuetext" | "accessibilityActions" | "aria-busy" | "aria-checked" | "aria-disabled" | "aria-expanded" | "aria-selected" | "aria-hidden" | "accessibilityLabelledBy" | "aria-labelledby" | "accessibilityLiveRegion" | "aria-live" | "importantForAccessibility" | "screenReaderFocusable" | "children" | "style" | "collapsable" | "collapsableChildren" | "id" | "testID" | "nativeID" | "needsOffscreenAlphaCompositing" | "hitSlop" | "pointerEvents" | "removeClippedSubviews" | "experimental_accessibilityOrder"> & Omit<Readonly<{
|
|
46
|
+
onClick?: ((event: import("react-native").PointerEvent) => void) | undefined;
|
|
47
|
+
onClickCapture?: ((event: import("react-native").PointerEvent) => void) | undefined;
|
|
48
|
+
onPointerEnter?: ((event: import("react-native").PointerEvent) => void) | undefined;
|
|
49
|
+
onPointerEnterCapture?: ((event: import("react-native").PointerEvent) => void) | undefined;
|
|
50
|
+
onPointerLeave?: ((event: import("react-native").PointerEvent) => void) | undefined;
|
|
51
|
+
onPointerLeaveCapture?: ((event: import("react-native").PointerEvent) => void) | undefined;
|
|
52
|
+
onPointerMove?: ((event: import("react-native").PointerEvent) => void) | undefined;
|
|
53
|
+
onPointerMoveCapture?: ((event: import("react-native").PointerEvent) => void) | undefined;
|
|
54
|
+
onPointerCancel?: ((e: import("react-native").PointerEvent) => void) | undefined;
|
|
55
|
+
onPointerCancelCapture?: ((e: import("react-native").PointerEvent) => void) | undefined;
|
|
56
|
+
onPointerDown?: ((e: import("react-native").PointerEvent) => void) | undefined;
|
|
57
|
+
onPointerDownCapture?: ((e: import("react-native").PointerEvent) => void) | undefined;
|
|
58
|
+
onPointerUp?: ((e: import("react-native").PointerEvent) => void) | undefined;
|
|
59
|
+
onPointerUpCapture?: ((e: import("react-native").PointerEvent) => void) | undefined;
|
|
60
|
+
onPointerOver?: ((e: import("react-native").PointerEvent) => void) | undefined;
|
|
61
|
+
onPointerOverCapture?: ((e: import("react-native").PointerEvent) => void) | undefined;
|
|
62
|
+
onPointerOut?: ((e: import("react-native").PointerEvent) => void) | undefined;
|
|
63
|
+
onPointerOutCapture?: ((e: import("react-native").PointerEvent) => void) | undefined;
|
|
64
|
+
onGotPointerCapture?: ((e: import("react-native").PointerEvent) => void) | undefined;
|
|
65
|
+
onGotPointerCaptureCapture?: ((e: import("react-native").PointerEvent) => void) | undefined;
|
|
66
|
+
onLostPointerCapture?: ((e: import("react-native").PointerEvent) => void) | undefined;
|
|
67
|
+
onLostPointerCaptureCapture?: ((e: import("react-native").PointerEvent) => void) | undefined;
|
|
68
|
+
}>, "onClick" | "onBlur" | "onBlurCapture" | "onFocus" | "onFocusCapture" | "onTouchCancel" | "onTouchCancelCapture" | "onTouchEnd" | "onTouchEndCapture" | "onTouchMove" | "onTouchMoveCapture" | "onTouchStart" | "onTouchStartCapture" | "nativeBackgroundAndroid" | "nativeForegroundAndroid" | "renderToHardwareTextureAndroid" | "hasTVPreferredFocus" | "nextFocusDown" | "nextFocusForward" | "nextFocusLeft" | "nextFocusRight" | "nextFocusUp" | "focusable" | "tabIndex" | "shouldRasterizeIOS" | "accessibilityIgnoresInvertColors" | "accessibilityViewIsModal" | "accessibilityShowsLargeContentViewer" | "accessibilityLargeContentTitle" | "aria-modal" | "accessibilityElementsHidden" | "accessibilityLanguage" | "accessibilityRespondsToUserInteraction" | "accessible" | "accessibilityLabel" | "accessibilityHint" | "aria-label" | "accessibilityRole" | "role" | "accessibilityState" | "accessibilityValue" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-valuetext" | "accessibilityActions" | "aria-busy" | "aria-checked" | "aria-disabled" | "aria-expanded" | "aria-selected" | "aria-hidden" | "accessibilityLabelledBy" | "aria-labelledby" | "accessibilityLiveRegion" | "aria-live" | "importantForAccessibility" | "screenReaderFocusable" | "children" | "style" | "collapsable" | "collapsableChildren" | "id" | "testID" | "nativeID" | "needsOffscreenAlphaCompositing" | "hitSlop" | "pointerEvents" | "removeClippedSubviews" | "experimental_accessibilityOrder"> & Omit<Readonly<{
|
|
69
|
+
onBlur?: ((event: import("react-native").BlurEvent) => void) | undefined;
|
|
70
|
+
onBlurCapture?: ((event: import("react-native").BlurEvent) => void) | undefined;
|
|
71
|
+
onFocus?: ((event: import("react-native").FocusEvent) => void) | undefined;
|
|
72
|
+
onFocusCapture?: ((event: import("react-native").FocusEvent) => void) | undefined;
|
|
73
|
+
}>, "onClick" | "onTouchCancel" | "onTouchCancelCapture" | "onTouchEnd" | "onTouchEndCapture" | "onTouchMove" | "onTouchMoveCapture" | "onTouchStart" | "onTouchStartCapture" | "nativeBackgroundAndroid" | "nativeForegroundAndroid" | "renderToHardwareTextureAndroid" | "hasTVPreferredFocus" | "nextFocusDown" | "nextFocusForward" | "nextFocusLeft" | "nextFocusRight" | "nextFocusUp" | "focusable" | "tabIndex" | "shouldRasterizeIOS" | "accessibilityIgnoresInvertColors" | "accessibilityViewIsModal" | "accessibilityShowsLargeContentViewer" | "accessibilityLargeContentTitle" | "aria-modal" | "accessibilityElementsHidden" | "accessibilityLanguage" | "accessibilityRespondsToUserInteraction" | "accessible" | "accessibilityLabel" | "accessibilityHint" | "aria-label" | "accessibilityRole" | "role" | "accessibilityState" | "accessibilityValue" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-valuetext" | "accessibilityActions" | "aria-busy" | "aria-checked" | "aria-disabled" | "aria-expanded" | "aria-selected" | "aria-hidden" | "accessibilityLabelledBy" | "aria-labelledby" | "accessibilityLiveRegion" | "aria-live" | "importantForAccessibility" | "screenReaderFocusable" | "children" | "style" | "collapsable" | "collapsableChildren" | "id" | "testID" | "nativeID" | "needsOffscreenAlphaCompositing" | "hitSlop" | "pointerEvents" | "removeClippedSubviews" | "experimental_accessibilityOrder"> & Omit<Readonly<{
|
|
74
|
+
onTouchCancel?: ((e: import("react-native").GestureResponderEvent) => void) | undefined;
|
|
75
|
+
onTouchCancelCapture?: ((e: import("react-native").GestureResponderEvent) => void) | undefined;
|
|
76
|
+
onTouchEnd?: ((e: import("react-native").GestureResponderEvent) => void) | undefined;
|
|
77
|
+
onTouchEndCapture?: ((e: import("react-native").GestureResponderEvent) => void) | undefined;
|
|
78
|
+
onTouchMove?: ((e: import("react-native").GestureResponderEvent) => void) | undefined;
|
|
79
|
+
onTouchMoveCapture?: ((e: import("react-native").GestureResponderEvent) => void) | undefined;
|
|
80
|
+
onTouchStart?: ((e: import("react-native").GestureResponderEvent) => void) | undefined;
|
|
81
|
+
onTouchStartCapture?: ((e: import("react-native").GestureResponderEvent) => void) | undefined;
|
|
82
|
+
}>, "onClick" | "nativeBackgroundAndroid" | "nativeForegroundAndroid" | "renderToHardwareTextureAndroid" | "hasTVPreferredFocus" | "nextFocusDown" | "nextFocusForward" | "nextFocusLeft" | "nextFocusRight" | "nextFocusUp" | "focusable" | "tabIndex" | "shouldRasterizeIOS" | "accessibilityIgnoresInvertColors" | "accessibilityViewIsModal" | "accessibilityShowsLargeContentViewer" | "accessibilityLargeContentTitle" | "aria-modal" | "accessibilityElementsHidden" | "accessibilityLanguage" | "accessibilityRespondsToUserInteraction" | "accessible" | "accessibilityLabel" | "accessibilityHint" | "aria-label" | "accessibilityRole" | "role" | "accessibilityState" | "accessibilityValue" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-valuetext" | "accessibilityActions" | "aria-busy" | "aria-checked" | "aria-disabled" | "aria-expanded" | "aria-selected" | "aria-hidden" | "accessibilityLabelledBy" | "aria-labelledby" | "accessibilityLiveRegion" | "aria-live" | "importantForAccessibility" | "screenReaderFocusable" | "children" | "style" | "collapsable" | "collapsableChildren" | "id" | "testID" | "nativeID" | "needsOffscreenAlphaCompositing" | "hitSlop" | "pointerEvents" | "removeClippedSubviews" | "experimental_accessibilityOrder"> & Omit<Readonly<{
|
|
83
|
+
nativeBackgroundAndroid?: import("react-native/types_generated/Libraries/Components/View/ViewPropTypes").AndroidDrawable | undefined;
|
|
84
|
+
nativeForegroundAndroid?: import("react-native/types_generated/Libraries/Components/View/ViewPropTypes").AndroidDrawable | undefined;
|
|
85
|
+
renderToHardwareTextureAndroid?: boolean | undefined;
|
|
86
|
+
hasTVPreferredFocus?: boolean | undefined;
|
|
87
|
+
nextFocusDown?: number | undefined;
|
|
88
|
+
nextFocusForward?: number | undefined;
|
|
89
|
+
nextFocusLeft?: number | undefined;
|
|
90
|
+
nextFocusRight?: number | undefined;
|
|
91
|
+
nextFocusUp?: number | undefined;
|
|
92
|
+
focusable?: boolean | undefined;
|
|
93
|
+
tabIndex?: 0 | -1;
|
|
94
|
+
onClick?: ((event: import("react-native").GestureResponderEvent) => unknown) | undefined;
|
|
95
|
+
}>, "shouldRasterizeIOS" | "accessibilityIgnoresInvertColors" | "accessibilityViewIsModal" | "accessibilityShowsLargeContentViewer" | "accessibilityLargeContentTitle" | "aria-modal" | "accessibilityElementsHidden" | "accessibilityLanguage" | "accessibilityRespondsToUserInteraction" | "accessible" | "accessibilityLabel" | "accessibilityHint" | "aria-label" | "accessibilityRole" | "role" | "accessibilityState" | "accessibilityValue" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-valuetext" | "accessibilityActions" | "aria-busy" | "aria-checked" | "aria-disabled" | "aria-expanded" | "aria-selected" | "aria-hidden" | "accessibilityLabelledBy" | "aria-labelledby" | "accessibilityLiveRegion" | "aria-live" | "importantForAccessibility" | "screenReaderFocusable" | "children" | "style" | "collapsable" | "collapsableChildren" | "id" | "testID" | "nativeID" | "needsOffscreenAlphaCompositing" | "hitSlop" | "pointerEvents" | "removeClippedSubviews" | "experimental_accessibilityOrder"> & Omit<Readonly<{
|
|
96
|
+
shouldRasterizeIOS?: boolean | undefined;
|
|
97
|
+
}>, "accessibilityIgnoresInvertColors" | "accessibilityViewIsModal" | "accessibilityShowsLargeContentViewer" | "accessibilityLargeContentTitle" | "aria-modal" | "accessibilityElementsHidden" | "accessibilityLanguage" | "accessibilityRespondsToUserInteraction" | "accessible" | "accessibilityLabel" | "accessibilityHint" | "aria-label" | "accessibilityRole" | "role" | "accessibilityState" | "accessibilityValue" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-valuetext" | "accessibilityActions" | "aria-busy" | "aria-checked" | "aria-disabled" | "aria-expanded" | "aria-selected" | "aria-hidden" | "accessibilityLabelledBy" | "aria-labelledby" | "accessibilityLiveRegion" | "aria-live" | "importantForAccessibility" | "screenReaderFocusable" | "children" | "style" | "collapsable" | "collapsableChildren" | "id" | "testID" | "nativeID" | "needsOffscreenAlphaCompositing" | "hitSlop" | "pointerEvents" | "removeClippedSubviews" | "experimental_accessibilityOrder"> & Omit<Readonly<Omit<Readonly<{
|
|
98
|
+
accessibilityLabelledBy?: (string | undefined) | (Array<string> | undefined);
|
|
99
|
+
"aria-labelledby"?: string | undefined;
|
|
100
|
+
accessibilityLiveRegion?: ("none" | "polite" | "assertive") | undefined;
|
|
101
|
+
"aria-live"?: ("polite" | "assertive" | "off") | undefined;
|
|
102
|
+
importantForAccessibility?: ("auto" | "yes" | "no" | "no-hide-descendants") | undefined;
|
|
103
|
+
screenReaderFocusable?: boolean;
|
|
104
|
+
}>, "accessibilityIgnoresInvertColors" | "accessibilityViewIsModal" | "accessibilityShowsLargeContentViewer" | "accessibilityLargeContentTitle" | "aria-modal" | "accessibilityElementsHidden" | "accessibilityLanguage" | "accessibilityRespondsToUserInteraction" | "accessible" | "accessibilityLabel" | "accessibilityHint" | "aria-label" | "accessibilityRole" | "role" | "accessibilityState" | "accessibilityValue" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-valuetext" | "accessibilityActions" | "aria-busy" | "aria-checked" | "aria-disabled" | "aria-expanded" | "aria-selected" | "aria-hidden"> & Omit<Readonly<{
|
|
105
|
+
accessibilityIgnoresInvertColors?: boolean | undefined;
|
|
106
|
+
accessibilityViewIsModal?: boolean | undefined;
|
|
107
|
+
accessibilityShowsLargeContentViewer?: boolean | undefined;
|
|
108
|
+
accessibilityLargeContentTitle?: string | undefined;
|
|
109
|
+
"aria-modal"?: boolean | undefined;
|
|
110
|
+
accessibilityElementsHidden?: boolean | undefined;
|
|
111
|
+
accessibilityLanguage?: string | undefined;
|
|
112
|
+
accessibilityRespondsToUserInteraction?: boolean | undefined;
|
|
113
|
+
}>, "accessible" | "accessibilityLabel" | "accessibilityHint" | "aria-label" | "accessibilityRole" | "role" | "accessibilityState" | "accessibilityValue" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-valuetext" | "accessibilityActions" | "aria-busy" | "aria-checked" | "aria-disabled" | "aria-expanded" | "aria-selected" | "aria-hidden"> & {
|
|
114
|
+
accessible?: boolean | undefined;
|
|
115
|
+
accessibilityLabel?: string | undefined;
|
|
116
|
+
accessibilityHint?: string | undefined;
|
|
117
|
+
"aria-label"?: string | undefined;
|
|
118
|
+
accessibilityRole?: import("react-native").AccessibilityRole | undefined;
|
|
119
|
+
role?: import("react-native").Role | undefined;
|
|
120
|
+
accessibilityState?: import("react-native").AccessibilityState | undefined;
|
|
121
|
+
accessibilityValue?: import("react-native").AccessibilityValue | undefined;
|
|
122
|
+
"aria-valuemax"?: import("react-native").AccessibilityValue["max"] | undefined;
|
|
123
|
+
"aria-valuemin"?: import("react-native").AccessibilityValue["min"] | undefined;
|
|
124
|
+
"aria-valuenow"?: import("react-native").AccessibilityValue["now"] | undefined;
|
|
125
|
+
"aria-valuetext"?: import("react-native").AccessibilityValue["text"] | undefined;
|
|
126
|
+
accessibilityActions?: ReadonlyArray<import("react-native/types_generated/Libraries/Components/View/ViewAccessibility").AccessibilityActionInfo> | undefined;
|
|
127
|
+
"aria-busy"?: boolean | undefined;
|
|
128
|
+
"aria-checked"?: (boolean | undefined) | "mixed";
|
|
129
|
+
"aria-disabled"?: boolean | undefined;
|
|
130
|
+
"aria-expanded"?: boolean | undefined;
|
|
131
|
+
"aria-selected"?: boolean | undefined;
|
|
132
|
+
"aria-hidden"?: boolean | undefined;
|
|
133
|
+
}>, "children" | "style" | "collapsable" | "collapsableChildren" | "id" | "testID" | "nativeID" | "needsOffscreenAlphaCompositing" | "hitSlop" | "pointerEvents" | "removeClippedSubviews" | "experimental_accessibilityOrder"> & Omit<Readonly<{
|
|
134
|
+
children?: React.ReactNode;
|
|
135
|
+
style?: import("react-native/types_generated/Libraries/StyleSheet/StyleSheet").ViewStyleProp | undefined;
|
|
136
|
+
collapsable?: boolean | undefined;
|
|
137
|
+
collapsableChildren?: boolean | undefined;
|
|
138
|
+
id?: string;
|
|
139
|
+
testID?: string | undefined;
|
|
140
|
+
nativeID?: string | undefined;
|
|
141
|
+
needsOffscreenAlphaCompositing?: boolean | undefined;
|
|
142
|
+
hitSlop?: import("react-native/types_generated/Libraries/StyleSheet/EdgeInsetsPropType").EdgeInsetsOrSizeProp | undefined;
|
|
143
|
+
pointerEvents?: ("auto" | "box-none" | "box-only" | "none") | undefined;
|
|
144
|
+
removeClippedSubviews?: boolean | undefined;
|
|
145
|
+
experimental_accessibilityOrder?: Array<string> | undefined;
|
|
146
|
+
}>, never>>, "experimental_accessibilityOrder">, "children" | "removeClippedSubviews" | "automaticallyAdjustContentInsets" | "automaticallyAdjustKeyboardInsets" | "automaticallyAdjustsScrollIndicatorInsets" | "contentInset" | "bounces" | "disableScrollViewPanResponder" | "bouncesZoom" | "alwaysBounceHorizontal" | "alwaysBounceVertical" | "centerContent" | "indicatorStyle" | "directionalLockEnabled" | "canCancelContentTouches" | "maximumZoomScale" | "minimumZoomScale" | "pinchGestureEnabled" | "scrollIndicatorInsets" | "scrollToOverflowEnabled" | "scrollsToTop" | "onScrollToTop" | "showsHorizontalScrollIndicator" | "zoomScale" | "contentInsetAdjustmentBehavior" | "nestedScrollEnabled" | "endFillColor" | "scrollPerfTag" | "overScrollMode" | "persistentScrollbar" | "fadingEdgeLength" | "contentContainerStyle" | "contentOffset" | "disableIntervalMomentum" | "decelerationRate" | "experimental_endDraggingSensitivityMultiplier" | "horizontal" | "invertStickyHeaders" | "keyboardDismissMode" | "keyboardShouldPersistTaps" | "maintainVisibleContentPosition" | "onMomentumScrollBegin" | "onMomentumScrollEnd" | "onScroll" | "onScrollBeginDrag" | "onScrollEndDrag" | "onContentSizeChange" | "onKeyboardDidShow" | "onKeyboardDidHide" | "onKeyboardWillShow" | "onKeyboardWillHide" | "pagingEnabled" | "scrollEnabled" | "scrollEventThrottle" | "showsVerticalScrollIndicator" | "stickyHeaderHiddenOnScroll" | "stickyHeaderIndices" | "StickyHeaderComponent" | "snapToAlignment" | "snapToInterval" | "snapToOffsets" | "snapToStart" | "snapToEnd" | "refreshControl" | "innerViewRef" | "scrollViewRef"> & Omit<Readonly<{
|
|
147
|
+
automaticallyAdjustContentInsets?: boolean | undefined;
|
|
148
|
+
automaticallyAdjustKeyboardInsets?: boolean | undefined;
|
|
149
|
+
automaticallyAdjustsScrollIndicatorInsets?: boolean | undefined;
|
|
150
|
+
contentInset?: import("react-native/types_generated/Libraries/StyleSheet/EdgeInsetsPropType").EdgeInsetsProp | undefined;
|
|
151
|
+
bounces?: boolean | undefined;
|
|
152
|
+
disableScrollViewPanResponder?: boolean | undefined;
|
|
153
|
+
bouncesZoom?: boolean | undefined;
|
|
154
|
+
alwaysBounceHorizontal?: boolean | undefined;
|
|
155
|
+
alwaysBounceVertical?: boolean | undefined;
|
|
156
|
+
centerContent?: boolean | undefined;
|
|
157
|
+
indicatorStyle?: ("default" | "black" | "white") | undefined;
|
|
158
|
+
directionalLockEnabled?: boolean | undefined;
|
|
159
|
+
canCancelContentTouches?: boolean | undefined;
|
|
160
|
+
maximumZoomScale?: number | undefined;
|
|
161
|
+
minimumZoomScale?: number | undefined;
|
|
162
|
+
pinchGestureEnabled?: boolean | undefined;
|
|
163
|
+
scrollIndicatorInsets?: import("react-native/types_generated/Libraries/StyleSheet/EdgeInsetsPropType").EdgeInsetsProp | undefined;
|
|
164
|
+
scrollToOverflowEnabled?: boolean | undefined;
|
|
165
|
+
scrollsToTop?: boolean | undefined;
|
|
166
|
+
onScrollToTop?: (event: import("react-native").ScrollEvent) => void;
|
|
167
|
+
showsHorizontalScrollIndicator?: boolean | undefined;
|
|
168
|
+
zoomScale?: number | undefined;
|
|
169
|
+
contentInsetAdjustmentBehavior?: ("automatic" | "scrollableAxes" | "never" | "always") | undefined;
|
|
170
|
+
}>, "children" | "removeClippedSubviews" | "nestedScrollEnabled" | "endFillColor" | "scrollPerfTag" | "overScrollMode" | "persistentScrollbar" | "fadingEdgeLength" | "contentContainerStyle" | "contentOffset" | "disableIntervalMomentum" | "decelerationRate" | "experimental_endDraggingSensitivityMultiplier" | "horizontal" | "invertStickyHeaders" | "keyboardDismissMode" | "keyboardShouldPersistTaps" | "maintainVisibleContentPosition" | "onMomentumScrollBegin" | "onMomentumScrollEnd" | "onScroll" | "onScrollBeginDrag" | "onScrollEndDrag" | "onContentSizeChange" | "onKeyboardDidShow" | "onKeyboardDidHide" | "onKeyboardWillShow" | "onKeyboardWillHide" | "pagingEnabled" | "scrollEnabled" | "scrollEventThrottle" | "showsVerticalScrollIndicator" | "stickyHeaderHiddenOnScroll" | "stickyHeaderIndices" | "StickyHeaderComponent" | "snapToAlignment" | "snapToInterval" | "snapToOffsets" | "snapToStart" | "snapToEnd" | "refreshControl" | "innerViewRef" | "scrollViewRef"> & Omit<Readonly<{
|
|
171
|
+
nestedScrollEnabled?: boolean | undefined;
|
|
172
|
+
endFillColor?: import("react-native").ColorValue | undefined;
|
|
173
|
+
scrollPerfTag?: string | undefined;
|
|
174
|
+
overScrollMode?: ("auto" | "always" | "never") | undefined;
|
|
175
|
+
persistentScrollbar?: boolean | undefined;
|
|
176
|
+
fadingEdgeLength?: (number | undefined) | {
|
|
177
|
+
start: number;
|
|
178
|
+
end: number;
|
|
179
|
+
};
|
|
180
|
+
}>, "children" | "removeClippedSubviews" | "contentContainerStyle" | "contentOffset" | "disableIntervalMomentum" | "decelerationRate" | "experimental_endDraggingSensitivityMultiplier" | "horizontal" | "invertStickyHeaders" | "keyboardDismissMode" | "keyboardShouldPersistTaps" | "maintainVisibleContentPosition" | "onMomentumScrollBegin" | "onMomentumScrollEnd" | "onScroll" | "onScrollBeginDrag" | "onScrollEndDrag" | "onContentSizeChange" | "onKeyboardDidShow" | "onKeyboardDidHide" | "onKeyboardWillShow" | "onKeyboardWillHide" | "pagingEnabled" | "scrollEnabled" | "scrollEventThrottle" | "showsVerticalScrollIndicator" | "stickyHeaderHiddenOnScroll" | "stickyHeaderIndices" | "StickyHeaderComponent" | "snapToAlignment" | "snapToInterval" | "snapToOffsets" | "snapToStart" | "snapToEnd" | "refreshControl" | "innerViewRef" | "scrollViewRef"> & Omit<Readonly<{
|
|
181
|
+
contentContainerStyle?: import("react-native/types_generated/Libraries/StyleSheet/StyleSheet").ViewStyleProp | undefined;
|
|
182
|
+
contentOffset?: import("react-native/types_generated/Libraries/StyleSheet/PointPropType").PointProp | undefined;
|
|
183
|
+
disableIntervalMomentum?: boolean | undefined;
|
|
184
|
+
decelerationRate?: import("react-native/types_generated/Libraries/Components/ScrollView/ScrollView").DecelerationRateType | undefined;
|
|
185
|
+
experimental_endDraggingSensitivityMultiplier?: number | undefined;
|
|
186
|
+
horizontal?: boolean | undefined;
|
|
187
|
+
invertStickyHeaders?: boolean | undefined;
|
|
188
|
+
keyboardDismissMode?: ("none" | "on-drag" | "interactive") | undefined;
|
|
189
|
+
keyboardShouldPersistTaps?: ("always" | "never" | "handled" | true | false) | undefined;
|
|
190
|
+
maintainVisibleContentPosition?: Readonly<{
|
|
191
|
+
minIndexForVisible: number;
|
|
192
|
+
autoscrollToTopThreshold?: number | undefined;
|
|
193
|
+
}> | undefined;
|
|
194
|
+
onMomentumScrollBegin?: ((event: import("react-native").ScrollEvent) => void) | undefined;
|
|
195
|
+
onMomentumScrollEnd?: ((event: import("react-native").ScrollEvent) => void) | undefined;
|
|
196
|
+
onScroll?: ((event: import("react-native").ScrollEvent) => void) | undefined;
|
|
197
|
+
onScrollBeginDrag?: ((event: import("react-native").ScrollEvent) => void) | undefined;
|
|
198
|
+
onScrollEndDrag?: ((event: import("react-native").ScrollEvent) => void) | undefined;
|
|
199
|
+
onContentSizeChange?: (contentWidth: number, contentHeight: number) => void;
|
|
200
|
+
onKeyboardDidShow?: (event: import("react-native").KeyboardEvent) => void;
|
|
201
|
+
onKeyboardDidHide?: (event: import("react-native").KeyboardEvent) => void;
|
|
202
|
+
onKeyboardWillShow?: (event: import("react-native").KeyboardEvent) => void;
|
|
203
|
+
onKeyboardWillHide?: (event: import("react-native").KeyboardEvent) => void;
|
|
204
|
+
pagingEnabled?: boolean | undefined;
|
|
205
|
+
scrollEnabled?: boolean | undefined;
|
|
206
|
+
scrollEventThrottle?: number | undefined;
|
|
207
|
+
showsVerticalScrollIndicator?: boolean | undefined;
|
|
208
|
+
stickyHeaderHiddenOnScroll?: boolean | undefined;
|
|
209
|
+
stickyHeaderIndices?: ReadonlyArray<number> | undefined;
|
|
210
|
+
StickyHeaderComponent?: (props: Omit<import("react-native/types_generated/Libraries/Components/ScrollView/ScrollViewStickyHeader").ScrollViewStickyHeaderProps, keyof {
|
|
211
|
+
ref?: React.Ref<Readonly<{
|
|
212
|
+
setNextHeaderY: ($$PARAM_0$$: number) => void;
|
|
213
|
+
}>>;
|
|
214
|
+
}> & {
|
|
215
|
+
ref?: React.Ref<Readonly<{
|
|
216
|
+
setNextHeaderY: ($$PARAM_0$$: number) => void;
|
|
217
|
+
}>>;
|
|
218
|
+
}) => React.ReactNode;
|
|
219
|
+
snapToAlignment?: ("start" | "center" | "end") | undefined;
|
|
220
|
+
snapToInterval?: number | undefined;
|
|
221
|
+
snapToOffsets?: ReadonlyArray<number> | undefined;
|
|
222
|
+
snapToStart?: boolean | undefined;
|
|
223
|
+
snapToEnd?: boolean | undefined;
|
|
224
|
+
removeClippedSubviews?: boolean | undefined;
|
|
225
|
+
refreshControl?: React.JSX.Element | undefined;
|
|
226
|
+
children?: React.ReactNode;
|
|
227
|
+
innerViewRef?: React.Ref<import("react-native/types_generated/src/private/webapis/dom/nodes/ReactNativeElement").default>;
|
|
228
|
+
scrollViewRef?: React.Ref<import("react-native/types_generated/Libraries/Components/ScrollView/ScrollView").PublicScrollViewInstance>;
|
|
229
|
+
}>, never>> & {
|
|
230
|
+
/**
|
|
231
|
+
* Fling velocity cap as a normalized fraction in [0, 1], or `null` to
|
|
232
|
+
* disable the cap mechanism entirely.
|
|
233
|
+
*
|
|
234
|
+
* The reference maximum is 8000 dp/s on Android and 8000 pt/s on iOS
|
|
235
|
+
* (same logical unit), so the same `maxVelocity` value produces the same
|
|
236
|
+
* visual scroll speed on both platforms.
|
|
237
|
+
*
|
|
238
|
+
* - `null` / `undefined` (default) — no capper installed; behaves exactly
|
|
239
|
+
* like a plain RN `ScrollView`.
|
|
240
|
+
* - `1` — capper installed at the reference maximum. Human-paced flings
|
|
241
|
+
* pass through untouched.
|
|
242
|
+
* - `0` — list cannot fling at all.
|
|
243
|
+
* - `0 < x < 1` — peak fling velocity is clamped to `x × 8000` (dp/s on
|
|
244
|
+
* Android, pt/s on iOS). Fling distance scales linearly with `x`.
|
|
245
|
+
*/
|
|
246
|
+
maxVelocity?: number | null;
|
|
247
|
+
} & import("react").RefAttributes<import("react-native/types_generated/Libraries/Components/ScrollView/ScrollView").PublicScrollViewInstance>>;
|
|
248
|
+
//# sourceMappingURL=CappedScrollView.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CappedScrollView.d.ts","sourceRoot":"","sources":["../../../src/CappedScrollView.tsx"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,YAAY,EAElB,MAAM,OAAO,CAAC;AACf,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAGhE,MAAM,MAAM,mBAAmB,GAAG,YAAY,CAAC,OAAO,UAAU,CAAC,CAAC;AAElE,MAAM,MAAM,qBAAqB,GAAG,eAAe,GAAG;IACpD;;;;;;;;;;;;;;;OAeG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B,CAAC;AAOF,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gCAmB4jb,CAAC;;;;;;;;;;;;;;;;;;;WAA74K,CAAC;;;;WAA6F,CAAC;;;;;;;;;;;;;;;IA3C1yQ;;;;;;;;;;;;;;;OAeG;kBACW,MAAM,GAAG,IAAI;8IA0B3B,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { TurboModule } from 'react-native';
|
|
2
|
+
export interface Spec extends TurboModule {
|
|
3
|
+
setMaxVelocity(reactTag: number, maxVelocity: number): void;
|
|
4
|
+
}
|
|
5
|
+
declare const _default: Spec;
|
|
6
|
+
export default _default;
|
|
7
|
+
//# sourceMappingURL=NativeCappedScrollView.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NativeCappedScrollView.d.ts","sourceRoot":"","sources":["../../../src/NativeCappedScrollView.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAGhD,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7D;;AAED,wBAA0E"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,YAAY,EACV,qBAAqB,EACrB,mBAAmB,GACpB,MAAM,oBAAoB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-capped-scrollview",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Drop-in React Native ScrollView that caps fling velocity via a 0-1 maxVelocity prop. Fabric + TurboModules, iOS and Android.",
|
|
5
|
+
"main": "./lib/module/index.js",
|
|
6
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"source": "./src/index.tsx",
|
|
10
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
11
|
+
"default": "./lib/module/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"lib",
|
|
18
|
+
"android",
|
|
19
|
+
"ios",
|
|
20
|
+
"cpp",
|
|
21
|
+
"*.podspec",
|
|
22
|
+
"react-native.config.js",
|
|
23
|
+
"!ios/build",
|
|
24
|
+
"!android/build",
|
|
25
|
+
"!android/gradle",
|
|
26
|
+
"!android/gradlew",
|
|
27
|
+
"!android/gradlew.bat",
|
|
28
|
+
"!android/local.properties",
|
|
29
|
+
"!**/__tests__",
|
|
30
|
+
"!**/__fixtures__",
|
|
31
|
+
"!**/__mocks__",
|
|
32
|
+
"!**/.*"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"example": "yarn workspace react-native-capped-scrollview-example",
|
|
36
|
+
"clean": "del-cli lib",
|
|
37
|
+
"prepare": "bob build",
|
|
38
|
+
"typecheck": "tsc",
|
|
39
|
+
"lint": "eslint \"**/*.{js,ts,tsx}\"",
|
|
40
|
+
"test": "jest",
|
|
41
|
+
"release": "release-it --only-version"
|
|
42
|
+
},
|
|
43
|
+
"keywords": [
|
|
44
|
+
"react-native",
|
|
45
|
+
"ios",
|
|
46
|
+
"android",
|
|
47
|
+
"scrollview",
|
|
48
|
+
"scroll",
|
|
49
|
+
"fling",
|
|
50
|
+
"velocity",
|
|
51
|
+
"max-velocity",
|
|
52
|
+
"fabric",
|
|
53
|
+
"turbomodule"
|
|
54
|
+
],
|
|
55
|
+
"repository": {
|
|
56
|
+
"type": "git",
|
|
57
|
+
"url": "git+https://github.com/leonsilicon/react-native-capped-scrollview.git"
|
|
58
|
+
},
|
|
59
|
+
"author": "Leon Si <leon@leonsilicon.com> (https://github.com/leonsilicon)",
|
|
60
|
+
"license": "MIT",
|
|
61
|
+
"bugs": {
|
|
62
|
+
"url": "https://github.com/leonsilicon/react-native-capped-scrollview/issues"
|
|
63
|
+
},
|
|
64
|
+
"homepage": "https://github.com/leonsilicon/react-native-capped-scrollview#readme",
|
|
65
|
+
"publishConfig": {
|
|
66
|
+
"registry": "https://registry.npmjs.org/"
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@commitlint/config-conventional": "^20.5.0",
|
|
70
|
+
"@eslint/compat": "^2.0.3",
|
|
71
|
+
"@eslint/eslintrc": "^3.3.5",
|
|
72
|
+
"@eslint/js": "^10.0.1",
|
|
73
|
+
"@jest/globals": "^30.0.0",
|
|
74
|
+
"@react-native/babel-preset": "0.85.0",
|
|
75
|
+
"@react-native/eslint-config": "0.85.0",
|
|
76
|
+
"@react-native/jest-preset": "0.85.0",
|
|
77
|
+
"@release-it/conventional-changelog": "^10.0.6",
|
|
78
|
+
"@types/react": "^19.2.0",
|
|
79
|
+
"commitlint": "^20.5.0",
|
|
80
|
+
"del-cli": "^7.0.0",
|
|
81
|
+
"eslint": "^9.39.4",
|
|
82
|
+
"eslint-config-prettier": "^10.1.8",
|
|
83
|
+
"eslint-plugin-ft-flow": "^3.0.11",
|
|
84
|
+
"eslint-plugin-prettier": "^5.5.5",
|
|
85
|
+
"jest": "^30.3.0",
|
|
86
|
+
"lefthook": "^2.1.4",
|
|
87
|
+
"prettier": "^3.8.1",
|
|
88
|
+
"react": "19.2.0",
|
|
89
|
+
"react-native": "0.83.6",
|
|
90
|
+
"react-native-builder-bob": "^0.41.0",
|
|
91
|
+
"release-it": "^19.2.4",
|
|
92
|
+
"turbo": "^2.8.21",
|
|
93
|
+
"typescript": "^6.0.2"
|
|
94
|
+
},
|
|
95
|
+
"peerDependencies": {
|
|
96
|
+
"react": ">=19.0.0",
|
|
97
|
+
"react-native": ">=0.80.0"
|
|
98
|
+
},
|
|
99
|
+
"engines": {
|
|
100
|
+
"node": ">=20"
|
|
101
|
+
},
|
|
102
|
+
"workspaces": [
|
|
103
|
+
"example"
|
|
104
|
+
],
|
|
105
|
+
"packageManager": "yarn@4.11.0",
|
|
106
|
+
"react-native-builder-bob": {
|
|
107
|
+
"source": "src",
|
|
108
|
+
"output": "lib",
|
|
109
|
+
"targets": [
|
|
110
|
+
[
|
|
111
|
+
"module",
|
|
112
|
+
{
|
|
113
|
+
"esm": true
|
|
114
|
+
}
|
|
115
|
+
],
|
|
116
|
+
[
|
|
117
|
+
"typescript",
|
|
118
|
+
{
|
|
119
|
+
"project": "tsconfig.build.json"
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
]
|
|
123
|
+
},
|
|
124
|
+
"codegenConfig": {
|
|
125
|
+
"name": "CappedScrollViewSpec",
|
|
126
|
+
"type": "modules",
|
|
127
|
+
"jsSrcsDir": "src",
|
|
128
|
+
"android": {
|
|
129
|
+
"javaPackageName": "com.cappedscrollview"
|
|
130
|
+
},
|
|
131
|
+
"ios": {
|
|
132
|
+
"modules": {
|
|
133
|
+
"CappedScrollView": {
|
|
134
|
+
"className": "CappedScrollView"
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
"prettier": {
|
|
140
|
+
"quoteProps": "consistent",
|
|
141
|
+
"singleQuote": true,
|
|
142
|
+
"tabWidth": 2,
|
|
143
|
+
"trailingComma": "es5",
|
|
144
|
+
"useTabs": false
|
|
145
|
+
},
|
|
146
|
+
"jest": {
|
|
147
|
+
"preset": "@react-native/jest-preset",
|
|
148
|
+
"modulePathIgnorePatterns": [
|
|
149
|
+
"<rootDir>/example/node_modules",
|
|
150
|
+
"<rootDir>/lib/"
|
|
151
|
+
]
|
|
152
|
+
},
|
|
153
|
+
"commitlint": {
|
|
154
|
+
"extends": [
|
|
155
|
+
"@commitlint/config-conventional"
|
|
156
|
+
]
|
|
157
|
+
},
|
|
158
|
+
"release-it": {
|
|
159
|
+
"git": {
|
|
160
|
+
"commitMessage": "chore: release ${version}",
|
|
161
|
+
"tagName": "v${version}"
|
|
162
|
+
},
|
|
163
|
+
"npm": {
|
|
164
|
+
"publish": true
|
|
165
|
+
},
|
|
166
|
+
"github": {
|
|
167
|
+
"release": true
|
|
168
|
+
},
|
|
169
|
+
"plugins": {
|
|
170
|
+
"@release-it/conventional-changelog": {
|
|
171
|
+
"preset": {
|
|
172
|
+
"name": "angular"
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
"create-react-native-library": {
|
|
178
|
+
"type": "turbo-module",
|
|
179
|
+
"languages": "kotlin-objc",
|
|
180
|
+
"tools": [
|
|
181
|
+
"eslint",
|
|
182
|
+
"jest",
|
|
183
|
+
"lefthook",
|
|
184
|
+
"release-it"
|
|
185
|
+
],
|
|
186
|
+
"version": "0.62.0"
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useEffect,
|
|
4
|
+
useRef,
|
|
5
|
+
useImperativeHandle,
|
|
6
|
+
type ComponentRef,
|
|
7
|
+
type Ref,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import { ScrollView, type ScrollViewProps } from 'react-native';
|
|
10
|
+
import NativeCappedScrollView from './NativeCappedScrollView';
|
|
11
|
+
|
|
12
|
+
export type CappedScrollViewRef = ComponentRef<typeof ScrollView>;
|
|
13
|
+
|
|
14
|
+
export type CappedScrollViewProps = ScrollViewProps & {
|
|
15
|
+
/**
|
|
16
|
+
* Fling velocity cap as a normalized fraction in [0, 1], or `null` to
|
|
17
|
+
* disable the cap mechanism entirely.
|
|
18
|
+
*
|
|
19
|
+
* The reference maximum is 8000 dp/s on Android and 8000 pt/s on iOS
|
|
20
|
+
* (same logical unit), so the same `maxVelocity` value produces the same
|
|
21
|
+
* visual scroll speed on both platforms.
|
|
22
|
+
*
|
|
23
|
+
* - `null` / `undefined` (default) — no capper installed; behaves exactly
|
|
24
|
+
* like a plain RN `ScrollView`.
|
|
25
|
+
* - `1` — capper installed at the reference maximum. Human-paced flings
|
|
26
|
+
* pass through untouched.
|
|
27
|
+
* - `0` — list cannot fling at all.
|
|
28
|
+
* - `0 < x < 1` — peak fling velocity is clamped to `x × 8000` (dp/s on
|
|
29
|
+
* Android, pt/s on iOS). Fling distance scales linearly with `x`.
|
|
30
|
+
*/
|
|
31
|
+
maxVelocity?: number | null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Sentinel sent to native when consumers pass `null`/omit the prop, meaning
|
|
35
|
+
// "no capper installed at all." Negative values are unreachable from the
|
|
36
|
+
// public [0, 1] range, so this is unambiguous on the native side.
|
|
37
|
+
const NO_CAP_SENTINEL = -1;
|
|
38
|
+
|
|
39
|
+
export const CappedScrollView = forwardRef(function CappedScrollView(
|
|
40
|
+
{ maxVelocity, ...rest }: CappedScrollViewProps,
|
|
41
|
+
ref: Ref<CappedScrollViewRef>
|
|
42
|
+
) {
|
|
43
|
+
const innerRef = useRef<CappedScrollViewRef | null>(null);
|
|
44
|
+
|
|
45
|
+
useImperativeHandle(ref, () => innerRef.current as CappedScrollViewRef);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
const handle = innerRef.current?.getScrollableNode?.();
|
|
49
|
+
if (handle == null) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const value = maxVelocity == null ? NO_CAP_SENTINEL : maxVelocity;
|
|
53
|
+
NativeCappedScrollView.setMaxVelocity(handle, value);
|
|
54
|
+
}, [maxVelocity]);
|
|
55
|
+
|
|
56
|
+
return <ScrollView ref={innerRef} {...rest} />;
|
|
57
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { TurboModule } from 'react-native';
|
|
2
|
+
import { TurboModuleRegistry } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export interface Spec extends TurboModule {
|
|
5
|
+
setMaxVelocity(reactTag: number, maxVelocity: number): void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default TurboModuleRegistry.getEnforcing<Spec>('CappedScrollView');
|