react-native-smooth-clip-view 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.
Files changed (49) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +170 -0
  3. package/SmoothClipView.podspec +24 -0
  4. package/android/build.gradle +63 -0
  5. package/android/src/main/AndroidManifest.xml +2 -0
  6. package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +93 -0
  7. package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +190 -0
  8. package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +104 -0
  9. package/android/src/main/java/com/smoothclipview/SmoothClipViewPackage.kt +21 -0
  10. package/ios/SmoothClipGeometry.h +41 -0
  11. package/ios/SmoothClipView.h +9 -0
  12. package/ios/SmoothClipView.mm +224 -0
  13. package/lib/module/SmoothClipView.js +35 -0
  14. package/lib/module/SmoothClipView.js.map +1 -0
  15. package/lib/module/SmoothClipView.web.js +38 -0
  16. package/lib/module/SmoothClipView.web.js.map +1 -0
  17. package/lib/module/SmoothClipViewNativeComponent.ts +34 -0
  18. package/lib/module/geometry.js +41 -0
  19. package/lib/module/geometry.js.map +1 -0
  20. package/lib/module/index.js +5 -0
  21. package/lib/module/index.js.map +1 -0
  22. package/lib/module/index.web.js +5 -0
  23. package/lib/module/index.web.js.map +1 -0
  24. package/lib/module/package.json +1 -0
  25. package/lib/module/webClipPath.js +25 -0
  26. package/lib/module/webClipPath.js.map +1 -0
  27. package/lib/typescript/package.json +1 -0
  28. package/lib/typescript/src/SmoothClipView.d.ts +12 -0
  29. package/lib/typescript/src/SmoothClipView.d.ts.map +1 -0
  30. package/lib/typescript/src/SmoothClipView.web.d.ts +12 -0
  31. package/lib/typescript/src/SmoothClipView.web.d.ts.map +1 -0
  32. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts +15 -0
  33. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts.map +1 -0
  34. package/lib/typescript/src/geometry.d.ts +19 -0
  35. package/lib/typescript/src/geometry.d.ts.map +1 -0
  36. package/lib/typescript/src/index.d.ts +5 -0
  37. package/lib/typescript/src/index.d.ts.map +1 -0
  38. package/lib/typescript/src/index.web.d.ts +5 -0
  39. package/lib/typescript/src/index.web.d.ts.map +1 -0
  40. package/lib/typescript/src/webClipPath.d.ts +7 -0
  41. package/lib/typescript/src/webClipPath.d.ts.map +1 -0
  42. package/package.json +201 -0
  43. package/src/SmoothClipView.tsx +72 -0
  44. package/src/SmoothClipView.web.tsx +62 -0
  45. package/src/SmoothClipViewNativeComponent.ts +34 -0
  46. package/src/geometry.ts +83 -0
  47. package/src/index.ts +4 -0
  48. package/src/index.web.ts +4 -0
  49. package/src/webClipPath.ts +30 -0
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kbrattli
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,170 @@
1
+ # react-native-smooth-clip-view
2
+
3
+ Layout-free animated rounded clipping for React Native Fabric.
4
+
5
+ `SmoothClipView` keeps a fixed maximum Yoga footprint while Reanimated sends
6
+ one atomic clip geometry update to the native view on the UI runtime. It is
7
+ useful for expanding cards, sheets, maps, media, and other reveals where
8
+ animating layout width and height would reflow descendants.
9
+
10
+ ## Requirements
11
+
12
+ - React Native 0.85 or newer with the New Architecture enabled
13
+ - React Native Reanimated 4.3 or newer
14
+ - iOS 16.4 or newer
15
+ - Android API 26 or newer
16
+
17
+ The package supports iOS, Android, and React Native Web. It does not include a
18
+ legacy Paper implementation.
19
+
20
+ ## Installation
21
+
22
+ ```sh
23
+ npm install react-native-smooth-clip-view react-native-reanimated react-native-worklets
24
+ ```
25
+
26
+ For iOS, install pods after adding the package:
27
+
28
+ ```sh
29
+ cd ios && pod install
30
+ ```
31
+
32
+ Follow the Reanimated installation instructions for your React Native setup.
33
+ Expo SDK 57 configures the required Babel plugin through `babel-preset-expo`.
34
+
35
+ ## Usage
36
+
37
+ ```tsx
38
+ import { useState } from 'react';
39
+ import { Button, StyleSheet, View } from 'react-native';
40
+ import {
41
+ type ClipGeometry,
42
+ SmoothClipView,
43
+ } from 'react-native-smooth-clip-view';
44
+ import {
45
+ interpolate,
46
+ useDerivedValue,
47
+ useSharedValue,
48
+ withTiming,
49
+ } from 'react-native-reanimated';
50
+
51
+ const initialClip: ClipGeometry = {
52
+ x: 120,
53
+ y: 180,
54
+ width: 80,
55
+ height: 80,
56
+ radius: 40,
57
+ };
58
+
59
+ export function ClipExample() {
60
+ const [expanded, setExpanded] = useState(false);
61
+ const progress = useSharedValue(0);
62
+ const animatedClip = useDerivedValue<ClipGeometry>(() => ({
63
+ x: interpolate(progress.value, [0, 1], [120, 0]),
64
+ y: interpolate(progress.value, [0, 1], [180, 0]),
65
+ width: interpolate(progress.value, [0, 1], [80, 320]),
66
+ height: interpolate(progress.value, [0, 1], [80, 480]),
67
+ radius: interpolate(progress.value, [0, 1], [40, 24]),
68
+ }));
69
+
70
+ const toggle = () => {
71
+ const next = !expanded;
72
+ setExpanded(next);
73
+ progress.value = withTiming(next ? 1 : 0);
74
+ };
75
+
76
+ return (
77
+ <View>
78
+ <SmoothClipView
79
+ initialClip={initialClip}
80
+ animatedClip={animatedClip}
81
+ style={styles.host}
82
+ >
83
+ <View style={styles.content} />
84
+ </SmoothClipView>
85
+ <Button title="Toggle clip" onPress={toggle} />
86
+ </View>
87
+ );
88
+ }
89
+
90
+ const styles = StyleSheet.create({
91
+ host: { width: 320, height: 480 },
92
+ content: { width: 320, height: 480, backgroundColor: '#112743' },
93
+ });
94
+ ```
95
+
96
+ `initialClip` must equal `animatedClip.value` when the component mounts. This
97
+ lets native rendering start with the correct clip before the first UI-runtime
98
+ command is dispatched.
99
+
100
+ ## API
101
+
102
+ ### `SmoothClipView`
103
+
104
+ `SmoothClipViewProps` extends React Native `ViewProps` and adds:
105
+
106
+ | Prop | Type | Description |
107
+ | --- | --- | --- |
108
+ | `initialClip` | `ClipGeometry` | Clip rendered synchronously at mount. |
109
+ | `animatedClip` | `SharedValue<ClipGeometry>` | Geometry observed on the Reanimated UI runtime. |
110
+ | `children` | `ReactNode` | Content rendered inside the fixed host. |
111
+
112
+ ### `ClipGeometry`
113
+
114
+ ```ts
115
+ type ClipGeometry = Readonly<{
116
+ x: number;
117
+ y: number;
118
+ width: number;
119
+ height: number;
120
+ radius: number;
121
+ }>;
122
+ ```
123
+
124
+ Values use React Native points/DIPs. Native code rejects non-finite updates,
125
+ intersects the requested rectangle with the actual host bounds, prevents
126
+ negative sizes, and clamps `radius` to half of the visible shortest edge.
127
+
128
+ ### `normalizeClipGeometry`
129
+
130
+ `normalizeClipGeometry(geometry, bounds)` mirrors the native normalization
131
+ contract for tests and non-native calculations. Native bounds remain
132
+ authoritative at render time.
133
+
134
+ ## Layout and styling contract
135
+
136
+ - Give the host its fixed maximum `width` and `height`; clipping never changes
137
+ Yoga layout.
138
+ - Put visual backgrounds inside `SmoothClipView`.
139
+ - Borders, shadows, host corner styles, host `overflow`, per-corner radii, and
140
+ externally applied anisotropic transforms are unsupported.
141
+ - Version 0.1.0 supports one uniform corner radius.
142
+ - On web, the implementation uses CSS `clip-path` on the fixed host, so
143
+ descendant percentages and right/bottom anchoring still resolve against the
144
+ maximum footprint.
145
+
146
+ ## Example and development
147
+
148
+ The [`example`](./example) workspace is an Expo SDK 57 app that exercises the
149
+ package through its public import on iOS, Android, and web.
150
+
151
+ ```sh
152
+ yarn
153
+ yarn example ios
154
+ yarn example android
155
+ yarn example web
156
+ ```
157
+
158
+ Run the repository checks with:
159
+
160
+ ```sh
161
+ yarn lint
162
+ yarn typecheck
163
+ yarn test
164
+ yarn prepare
165
+ yarn example build:web
166
+ ```
167
+
168
+ ## License
169
+
170
+ MIT
@@ -0,0 +1,24 @@
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 = "SmoothClipView"
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 => "16.4" }
14
+ s.source = { :git => "https://github.com/kbrattli/react-native-smooth-clip-view.git", :tag => "v#{s.version}" }
15
+ s.source_files = "ios/*.{h,m,mm,swift,cpp}"
16
+ s.private_header_files = "ios/*.h"
17
+
18
+ install_modules_dependencies(s)
19
+
20
+ s.test_spec "Tests" do |test_spec|
21
+ test_spec.framework = "XCTest"
22
+ test_spec.source_files = "ios/tests/**/*.{m,mm}"
23
+ end
24
+ end
@@ -0,0 +1,63 @@
1
+ buildscript {
2
+ ext.SmoothClipView = [
3
+ kotlinVersion: "2.0.21",
4
+ minSdkVersion: 26,
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
+ return SmoothClipView[prop]
14
+ }
15
+
16
+ repositories {
17
+ google()
18
+ mavenCentral()
19
+ }
20
+
21
+ dependencies {
22
+ classpath "com.android.tools.build:gradle:8.7.2"
23
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
24
+ }
25
+ }
26
+
27
+ apply plugin: "com.android.library"
28
+ apply plugin: "kotlin-android"
29
+ apply plugin: "com.facebook.react"
30
+
31
+ android {
32
+ namespace "com.smoothclipview"
33
+ compileSdkVersion getExtOrDefault("compileSdkVersion")
34
+
35
+ defaultConfig {
36
+ minSdkVersion getExtOrDefault("minSdkVersion")
37
+ targetSdkVersion getExtOrDefault("targetSdkVersion")
38
+ }
39
+
40
+ buildFeatures {
41
+ buildConfig true
42
+ }
43
+
44
+ buildTypes {
45
+ release {
46
+ minifyEnabled false
47
+ }
48
+ }
49
+
50
+ lint {
51
+ disable "GradleCompatible"
52
+ }
53
+
54
+ compileOptions {
55
+ sourceCompatibility JavaVersion.VERSION_1_8
56
+ targetCompatibility JavaVersion.VERSION_1_8
57
+ }
58
+ }
59
+
60
+ dependencies {
61
+ implementation "com.facebook.react:react-android"
62
+ testImplementation "junit:junit:4.13.2"
63
+ }
@@ -0,0 +1,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -0,0 +1,93 @@
1
+ package com.smoothclipview
2
+
3
+ import android.view.View
4
+ import kotlin.math.max
5
+ import kotlin.math.min
6
+
7
+ /**
8
+ * Normalizes clip geometry without allocating a result object. The callback is
9
+ * inlined into the view's command path so per-frame updates stay allocation-free.
10
+ */
11
+ internal inline fun normalizeClipGeometryPx(
12
+ x: Float,
13
+ y: Float,
14
+ width: Float,
15
+ height: Float,
16
+ radius: Float,
17
+ hostWidth: Float,
18
+ hostHeight: Float,
19
+ apply: (
20
+ left: Float,
21
+ top: Float,
22
+ right: Float,
23
+ bottom: Float,
24
+ radius: Float,
25
+ ) -> Unit,
26
+ ): Boolean {
27
+ if (!x.isFinite() || !y.isFinite() || !width.isFinite() ||
28
+ !height.isFinite() || !radius.isFinite() ||
29
+ !hostWidth.isFinite() || !hostHeight.isFinite()
30
+ ) {
31
+ return false
32
+ }
33
+
34
+ val boundedHostWidth = hostWidth.coerceAtLeast(0f)
35
+ val boundedHostHeight = hostHeight.coerceAtLeast(0f)
36
+ val requestedRight = x + width.coerceAtLeast(0f)
37
+ val requestedBottom = y + height.coerceAtLeast(0f)
38
+ val left = x.coerceIn(0f, boundedHostWidth)
39
+ val top = y.coerceIn(0f, boundedHostHeight)
40
+ val right = requestedRight.coerceIn(0f, boundedHostWidth)
41
+ val bottom = requestedBottom.coerceIn(0f, boundedHostHeight)
42
+ val visibleWidth = max(0f, right - left)
43
+ val visibleHeight = max(0f, bottom - top)
44
+ val normalizedRadius = min(
45
+ radius.coerceAtLeast(0f),
46
+ min(visibleWidth, visibleHeight) / 2f,
47
+ )
48
+
49
+ apply(left, top, right, bottom, normalizedRadius)
50
+ return true
51
+ }
52
+
53
+ internal fun clipVisibility(isEmpty: Boolean): Int =
54
+ if (isEmpty) View.INVISIBLE else View.VISIBLE
55
+
56
+ internal fun clipAccessibility(
57
+ isEmpty: Boolean,
58
+ requestedAccessibility: Int,
59
+ ): Int =
60
+ if (isEmpty) {
61
+ View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
62
+ } else {
63
+ requestedAccessibility
64
+ }
65
+
66
+ internal fun containsRoundedPointPx(
67
+ x: Float,
68
+ y: Float,
69
+ left: Float,
70
+ top: Float,
71
+ right: Float,
72
+ bottom: Float,
73
+ radius: Float,
74
+ isEmpty: Boolean,
75
+ ): Boolean {
76
+ if (isEmpty || x < left || x > right || y < top || y > bottom) {
77
+ return false
78
+ }
79
+
80
+ if (radius <= 0f) return true
81
+
82
+ val innerLeft = left + radius
83
+ val innerRight = right - radius
84
+ val innerTop = top + radius
85
+ val innerBottom = bottom - radius
86
+ if (x in innerLeft..innerRight || y in innerTop..innerBottom) return true
87
+
88
+ val centerX = if (x < innerLeft) innerLeft else innerRight
89
+ val centerY = if (y < innerTop) innerTop else innerBottom
90
+ val dx = x - centerX
91
+ val dy = y - centerY
92
+ return dx * dx + dy * dy <= radius * radius
93
+ }
@@ -0,0 +1,190 @@
1
+ package com.smoothclipview
2
+
3
+ import android.graphics.Outline
4
+ import android.view.MotionEvent
5
+ import android.view.View
6
+ import android.view.ViewOutlineProvider
7
+ import com.facebook.react.uimanager.PixelUtil
8
+ import com.facebook.react.uimanager.ThemedReactContext
9
+ import com.facebook.react.views.view.ReactViewGroup
10
+ import kotlin.math.roundToInt
11
+
12
+ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
13
+ private var requestedX = 0f
14
+ private var requestedY = 0f
15
+ private var requestedWidth = 0f
16
+ private var requestedHeight = 0f
17
+ private var requestedRadius = 0f
18
+ private var clipLeft = 0f
19
+ private var clipTop = 0f
20
+ private var clipRight = 0f
21
+ private var clipBottom = 0f
22
+ private var clipRadius = 0f
23
+ private var clipIsEmpty = true
24
+ private var requestedImportantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_AUTO
25
+ private var acceptingTouchStream = false
26
+
27
+ private val clipOutlineProvider = object : ViewOutlineProvider() {
28
+ override fun getOutline(view: View, outline: Outline) {
29
+ if (clipIsEmpty) {
30
+ outline.setEmpty()
31
+ return
32
+ }
33
+
34
+ outline.setRoundRect(
35
+ clipLeft.roundToInt(),
36
+ clipTop.roundToInt(),
37
+ clipRight.roundToInt(),
38
+ clipBottom.roundToInt(),
39
+ clipRadius,
40
+ )
41
+ }
42
+ }
43
+
44
+ init {
45
+ outlineProvider = clipOutlineProvider
46
+ clipToOutline = true
47
+ visibility = INVISIBLE
48
+ importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
49
+ }
50
+
51
+ fun setClipGeometryDip(
52
+ x: Double,
53
+ y: Double,
54
+ width: Double,
55
+ height: Double,
56
+ radius: Double,
57
+ ) {
58
+ if (!x.isFinite() || !y.isFinite() || !width.isFinite() ||
59
+ !height.isFinite() || !radius.isFinite()
60
+ ) {
61
+ return
62
+ }
63
+
64
+ val nextX = PixelUtil.toPixelFromDIP(x).toFloat()
65
+ val nextY = PixelUtil.toPixelFromDIP(y).toFloat()
66
+ val nextWidth = PixelUtil.toPixelFromDIP(width).toFloat()
67
+ val nextHeight = PixelUtil.toPixelFromDIP(height).toFloat()
68
+ val nextRadius = PixelUtil.toPixelFromDIP(radius).toFloat()
69
+ if (!nextX.isFinite() || !nextY.isFinite() || !nextWidth.isFinite() ||
70
+ !nextHeight.isFinite() || !nextRadius.isFinite()
71
+ ) {
72
+ return
73
+ }
74
+
75
+ requestedX = nextX
76
+ requestedY = nextY
77
+ requestedWidth = nextWidth
78
+ requestedHeight = nextHeight
79
+ requestedRadius = nextRadius
80
+ applyRequestedGeometry()
81
+ }
82
+
83
+ private fun applyRequestedGeometry() {
84
+ normalizeClipGeometryPx(
85
+ requestedX,
86
+ requestedY,
87
+ requestedWidth,
88
+ requestedHeight,
89
+ requestedRadius,
90
+ width.toFloat(),
91
+ height.toFloat(),
92
+ ) { left, top, right, bottom, radius ->
93
+ val isEmpty = right <= left || bottom <= top
94
+ val geometryChanged =
95
+ left != clipLeft || top != clipTop || right != clipRight ||
96
+ bottom != clipBottom || radius != clipRadius
97
+
98
+ if (!geometryChanged && isEmpty == clipIsEmpty) {
99
+ return@normalizeClipGeometryPx
100
+ }
101
+
102
+ clipLeft = left
103
+ clipTop = top
104
+ clipRight = right
105
+ clipBottom = bottom
106
+ clipRadius = radius
107
+
108
+ if (isEmpty != clipIsEmpty) {
109
+ clipIsEmpty = isEmpty
110
+ visibility = clipVisibility(isEmpty)
111
+ importantForAccessibility = clipAccessibility(
112
+ isEmpty,
113
+ requestedImportantForAccessibility,
114
+ )
115
+ if (isEmpty) acceptingTouchStream = false
116
+ }
117
+
118
+ invalidateOutline()
119
+ }
120
+ }
121
+
122
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
123
+ super.onSizeChanged(w, h, oldw, oldh)
124
+ applyRequestedGeometry()
125
+ }
126
+
127
+ private fun containsRoundedPoint(x: Float, y: Float): Boolean {
128
+ return containsRoundedPointPx(
129
+ x,
130
+ y,
131
+ clipLeft,
132
+ clipTop,
133
+ clipRight,
134
+ clipBottom,
135
+ clipRadius,
136
+ clipIsEmpty,
137
+ )
138
+ }
139
+
140
+ override fun dispatchTouchEvent(event: MotionEvent): Boolean {
141
+ when (event.actionMasked) {
142
+ MotionEvent.ACTION_DOWN -> {
143
+ acceptingTouchStream = containsRoundedPoint(event.x, event.y)
144
+ if (!acceptingTouchStream) return false
145
+ }
146
+ MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
147
+ val result = if (acceptingTouchStream) {
148
+ super.dispatchTouchEvent(event)
149
+ } else {
150
+ false
151
+ }
152
+ acceptingTouchStream = false
153
+ return result
154
+ }
155
+ }
156
+
157
+ return acceptingTouchStream && super.dispatchTouchEvent(event)
158
+ }
159
+
160
+ fun setRequestedImportantForAccessibility(value: Int) {
161
+ requestedImportantForAccessibility = value
162
+ reapplyClipPresentation()
163
+ }
164
+
165
+ fun reapplyClipPresentation() {
166
+ val expectedVisibility = clipVisibility(clipIsEmpty)
167
+ if (visibility != expectedVisibility) {
168
+ visibility = expectedVisibility
169
+ }
170
+
171
+ val expectedAccessibility = clipAccessibility(
172
+ clipIsEmpty,
173
+ requestedImportantForAccessibility,
174
+ )
175
+ if (importantForAccessibility != expectedAccessibility) {
176
+ importantForAccessibility = expectedAccessibility
177
+ }
178
+ }
179
+
180
+ fun resetClipState() {
181
+ acceptingTouchStream = false
182
+ requestedX = 0f
183
+ requestedY = 0f
184
+ requestedWidth = 0f
185
+ requestedHeight = 0f
186
+ requestedRadius = 0f
187
+ requestedImportantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_AUTO
188
+ applyRequestedGeometry()
189
+ }
190
+ }
@@ -0,0 +1,104 @@
1
+ package com.smoothclipview
2
+
3
+ import com.facebook.react.module.annotations.ReactModule
4
+ import com.facebook.react.uimanager.ThemedReactContext
5
+ import com.facebook.react.uimanager.ViewGroupManager
6
+ import com.facebook.react.uimanager.ViewManagerDelegate
7
+ import com.facebook.react.uimanager.annotations.ReactProp
8
+ import com.facebook.react.viewmanagers.SmoothClipViewManagerDelegate
9
+ import com.facebook.react.viewmanagers.SmoothClipViewManagerInterface
10
+
11
+ @ReactModule(name = SmoothClipViewManager.NAME)
12
+ class SmoothClipViewManager : ViewGroupManager<SmoothClipView>(),
13
+ SmoothClipViewManagerInterface<SmoothClipView> {
14
+ private val delegate =
15
+ SmoothClipViewManagerDelegate<SmoothClipView, SmoothClipViewManager>(this)
16
+
17
+ private data class PendingInitialClip(
18
+ var x: Double = 0.0,
19
+ var y: Double = 0.0,
20
+ var width: Double = 0.0,
21
+ var height: Double = 0.0,
22
+ var radius: Double = 0.0,
23
+ )
24
+
25
+ private val pendingInitialClips = java.util.WeakHashMap<SmoothClipView, PendingInitialClip>()
26
+
27
+ override fun getName(): String = NAME
28
+
29
+ override fun getDelegate(): ViewManagerDelegate<SmoothClipView> = delegate
30
+
31
+ override fun createViewInstance(context: ThemedReactContext): SmoothClipView =
32
+ SmoothClipView(context).also { pendingInitialClips[it] = PendingInitialClip() }
33
+
34
+ override fun setImportantForAccessibility(
35
+ view: SmoothClipView,
36
+ importantForAccessibility: String?,
37
+ ) {
38
+ super.setImportantForAccessibility(view, importantForAccessibility)
39
+ view.setRequestedImportantForAccessibility(view.importantForAccessibility)
40
+ }
41
+
42
+ @ReactProp(name = "initialClipX")
43
+ override fun setInitialClipX(view: SmoothClipView, value: Double) {
44
+ pendingInitialClips.getOrPut(view) { PendingInitialClip() }.x = value
45
+ }
46
+
47
+ @ReactProp(name = "initialClipY")
48
+ override fun setInitialClipY(view: SmoothClipView, value: Double) {
49
+ pendingInitialClips.getOrPut(view) { PendingInitialClip() }.y = value
50
+ }
51
+
52
+ @ReactProp(name = "initialClipWidth")
53
+ override fun setInitialClipWidth(view: SmoothClipView, value: Double) {
54
+ pendingInitialClips.getOrPut(view) { PendingInitialClip() }.width = value
55
+ }
56
+
57
+ @ReactProp(name = "initialClipHeight")
58
+ override fun setInitialClipHeight(view: SmoothClipView, value: Double) {
59
+ pendingInitialClips.getOrPut(view) { PendingInitialClip() }.height = value
60
+ }
61
+
62
+ @ReactProp(name = "initialClipRadius")
63
+ override fun setInitialClipRadius(view: SmoothClipView, value: Double) {
64
+ pendingInitialClips.getOrPut(view) { PendingInitialClip() }.radius = value
65
+ }
66
+
67
+ override fun setClipGeometry(
68
+ view: SmoothClipView,
69
+ x: Double,
70
+ y: Double,
71
+ width: Double,
72
+ height: Double,
73
+ radius: Double,
74
+ ) {
75
+ pendingInitialClips.remove(view)
76
+ view.setClipGeometryDip(x, y, width, height, radius)
77
+ }
78
+
79
+ override fun onAfterUpdateTransaction(view: SmoothClipView) {
80
+ super.onAfterUpdateTransaction(view)
81
+ val initial = pendingInitialClips.remove(view)
82
+ if (initial == null) {
83
+ view.reapplyClipPresentation()
84
+ } else {
85
+ view.setClipGeometryDip(
86
+ initial.x,
87
+ initial.y,
88
+ initial.width,
89
+ initial.height,
90
+ initial.radius,
91
+ )
92
+ }
93
+ }
94
+
95
+ override fun onDropViewInstance(view: SmoothClipView) {
96
+ pendingInitialClips.remove(view)
97
+ view.resetClipState()
98
+ super.onDropViewInstance(view)
99
+ }
100
+
101
+ companion object {
102
+ const val NAME = "SmoothClipView"
103
+ }
104
+ }
@@ -0,0 +1,21 @@
1
+ package com.smoothclipview
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.ReactModuleInfoProvider
7
+ import com.facebook.react.uimanager.ViewManager
8
+
9
+ class SmoothClipViewPackage : BaseReactPackage() {
10
+ override fun createViewManagers(
11
+ reactContext: ReactApplicationContext,
12
+ ): List<ViewManager<*, *>> = listOf(SmoothClipViewManager())
13
+
14
+ override fun getModule(
15
+ name: String,
16
+ reactContext: ReactApplicationContext,
17
+ ): NativeModule? = null
18
+
19
+ override fun getReactModuleInfoProvider() =
20
+ ReactModuleInfoProvider { emptyMap() }
21
+ }