react-native-controlled-input 0.12.2 → 0.12.4
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/android/build.gradle
CHANGED
|
@@ -89,5 +89,6 @@ dependencies {
|
|
|
89
89
|
implementation 'androidx.compose.material3:material3'
|
|
90
90
|
implementation 'androidx.activity:activity-compose:1.12.2'
|
|
91
91
|
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.7'
|
|
92
|
+
implementation 'androidx.savedstate:savedstate-ktx:1.2.1'
|
|
92
93
|
debugImplementation 'androidx.compose.ui:ui-tooling'
|
|
93
94
|
}
|
|
@@ -2,8 +2,10 @@ package com.controlledinput
|
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
4
|
import android.util.AttributeSet
|
|
5
|
+
import android.view.View
|
|
5
6
|
import android.view.inputmethod.InputMethodManager
|
|
6
7
|
import android.widget.LinearLayout
|
|
8
|
+
import androidx.annotation.UiThread
|
|
7
9
|
import androidx.compose.runtime.LaunchedEffect
|
|
8
10
|
import androidx.compose.runtime.collectAsState
|
|
9
11
|
import androidx.compose.runtime.getValue
|
|
@@ -15,11 +17,22 @@ import androidx.compose.ui.platform.ViewCompositionStrategy
|
|
|
15
17
|
import androidx.lifecycle.Lifecycle
|
|
16
18
|
import androidx.lifecycle.LifecycleOwner
|
|
17
19
|
import androidx.lifecycle.LifecycleRegistry
|
|
20
|
+
import androidx.lifecycle.findViewTreeLifecycleOwner
|
|
18
21
|
import androidx.lifecycle.setViewTreeLifecycleOwner
|
|
22
|
+
import androidx.savedstate.SavedStateRegistryOwner
|
|
23
|
+
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
|
|
19
24
|
import com.facebook.react.bridge.ReactContext
|
|
20
25
|
import com.facebook.react.uimanager.UIManagerHelper
|
|
21
26
|
import kotlinx.coroutines.flow.MutableStateFlow
|
|
22
27
|
|
|
28
|
+
/**
|
|
29
|
+
* RN + Jetpack Compose hosting view aligned with Expo UI / [ExpoComposeView] + [ExpoView]:
|
|
30
|
+
* - [shouldUseAndroidLayout]: requestLayout posts measureAndLayout (RN #17968)
|
|
31
|
+
* - onMeasure skips child [ComposeView] until attached (window + WindowRecomposer)
|
|
32
|
+
*
|
|
33
|
+
* @see expo.modules.kotlin.views.ExpoComposeView
|
|
34
|
+
* @see expo.modules.kotlin.views.ExpoView
|
|
35
|
+
*/
|
|
23
36
|
class ControlledInputView : LinearLayout, LifecycleOwner {
|
|
24
37
|
constructor(context: Context) : super(context) {
|
|
25
38
|
configureComponent(context)
|
|
@@ -43,69 +56,137 @@ class ControlledInputView : LinearLayout, LifecycleOwner {
|
|
|
43
56
|
internal lateinit var viewModel: JetpackComposeViewModel
|
|
44
57
|
private val blurSignal = MutableStateFlow(0)
|
|
45
58
|
private val focusSignal = MutableStateFlow(0)
|
|
59
|
+
private lateinit var composeView: ComposeView
|
|
60
|
+
private var usesLocalFallbackLifecycle = false
|
|
61
|
+
private var windowLifecycleBound = false
|
|
62
|
+
|
|
63
|
+
/** Same role as ExpoComposeView(withHostingView = true) → ExpoView.shouldUseAndroidLayout */
|
|
64
|
+
private val shouldUseAndroidLayout = true
|
|
65
|
+
|
|
66
|
+
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
67
|
+
// ExpoComposeView.onMeasure — do not measure ComposeView until attached to a window.
|
|
68
|
+
if (shouldUseAndroidLayout && !isAttachedToWindow) {
|
|
69
|
+
setMeasuredDimension(
|
|
70
|
+
MeasureSpec.getSize(widthMeasureSpec).coerceAtLeast(0),
|
|
71
|
+
MeasureSpec.getSize(heightMeasureSpec).coerceAtLeast(0)
|
|
72
|
+
)
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* ExpoView.requestLayout / measureAndLayout — Fabric/Yoga often won't drive Android layout
|
|
80
|
+
* for native children; this mirrors expo-modules-core behavior.
|
|
81
|
+
*/
|
|
82
|
+
override fun requestLayout() {
|
|
83
|
+
super.requestLayout()
|
|
84
|
+
if (shouldUseAndroidLayout) {
|
|
85
|
+
post { measureAndLayout() }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@UiThread
|
|
90
|
+
private fun measureAndLayout() {
|
|
91
|
+
measure(
|
|
92
|
+
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
|
93
|
+
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
|
|
94
|
+
)
|
|
95
|
+
layout(left, top, right, bottom)
|
|
96
|
+
}
|
|
46
97
|
|
|
47
98
|
override fun onAttachedToWindow() {
|
|
48
|
-
setViewTreeLifecycleOwner(this)
|
|
49
|
-
lifecycleRegistry.currentState = Lifecycle.State.RESUMED
|
|
50
99
|
super.onAttachedToWindow()
|
|
100
|
+
bindComposeToWindowLifecycle()
|
|
51
101
|
}
|
|
52
102
|
|
|
53
103
|
override fun onDetachedFromWindow() {
|
|
104
|
+
if (usesLocalFallbackLifecycle) {
|
|
105
|
+
lifecycleRegistry.currentState = Lifecycle.State.CREATED
|
|
106
|
+
}
|
|
54
107
|
super.onDetachedFromWindow()
|
|
55
|
-
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private fun bindComposeToWindowLifecycle() {
|
|
111
|
+
if (windowLifecycleBound) {
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
windowLifecycleBound = true
|
|
115
|
+
|
|
116
|
+
val activity = (context as? ReactContext)?.currentActivity
|
|
117
|
+
val activityOwner = activity as? LifecycleOwner
|
|
118
|
+
if (activityOwner != null) {
|
|
119
|
+
usesLocalFallbackLifecycle = false
|
|
120
|
+
composeView.setViewTreeLifecycleOwner(activityOwner)
|
|
121
|
+
val savedStateOwner = activity as? SavedStateRegistryOwner
|
|
122
|
+
if (savedStateOwner != null) {
|
|
123
|
+
composeView.setViewTreeSavedStateRegistryOwner(savedStateOwner)
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
findViewTreeLifecycleOwnerFromAncestors()?.let { parentOwner ->
|
|
127
|
+
usesLocalFallbackLifecycle = false
|
|
128
|
+
composeView.setViewTreeLifecycleOwner(parentOwner)
|
|
129
|
+
} ?: run {
|
|
130
|
+
usesLocalFallbackLifecycle = true
|
|
131
|
+
composeView.setViewTreeLifecycleOwner(this)
|
|
132
|
+
lifecycleRegistry.currentState = Lifecycle.State.RESUMED
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private fun findViewTreeLifecycleOwnerFromAncestors(): LifecycleOwner? {
|
|
138
|
+
var parent = this.parent as? View ?: return null
|
|
139
|
+
while (true) {
|
|
140
|
+
parent.findViewTreeLifecycleOwner()?.let {
|
|
141
|
+
return it
|
|
142
|
+
}
|
|
143
|
+
parent = parent.parent as? View ?: return null
|
|
144
|
+
}
|
|
56
145
|
}
|
|
57
146
|
|
|
58
147
|
fun blur() {
|
|
59
|
-
// триггерим compose снять фокус
|
|
60
148
|
blurSignal.value = blurSignal.value + 1
|
|
61
|
-
|
|
62
|
-
// на всякий случай прячем клавиатуру на уровне View
|
|
63
149
|
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
|
64
150
|
imm.hideSoftInputFromWindow(windowToken, 0)
|
|
65
|
-
|
|
66
|
-
// и снимаем фокус у самого android view (не всегда достаточно, но не мешает)
|
|
67
151
|
clearFocus()
|
|
68
152
|
}
|
|
69
153
|
|
|
70
154
|
fun focus() {
|
|
71
|
-
// триггерим compose запросить фокус
|
|
72
155
|
focusSignal.value = focusSignal.value + 1
|
|
73
156
|
}
|
|
74
157
|
|
|
75
158
|
private fun configureComponent(context: Context) {
|
|
76
159
|
setBackgroundColor(android.graphics.Color.TRANSPARENT)
|
|
160
|
+
clipChildren = false
|
|
161
|
+
clipToPadding = false
|
|
77
162
|
|
|
78
163
|
layoutParams = LayoutParams(
|
|
79
164
|
LayoutParams.MATCH_PARENT,
|
|
80
165
|
LayoutParams.MATCH_PARENT
|
|
81
166
|
)
|
|
82
167
|
|
|
83
|
-
|
|
84
|
-
|
|
168
|
+
viewModel = JetpackComposeViewModel()
|
|
169
|
+
|
|
170
|
+
composeView = ComposeView(context).also { cv ->
|
|
171
|
+
cv.layoutParams = LayoutParams(
|
|
85
172
|
LayoutParams.MATCH_PARENT,
|
|
86
173
|
LayoutParams.MATCH_PARENT
|
|
87
174
|
)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
viewModel = JetpackComposeViewModel()
|
|
93
|
-
|
|
94
|
-
it.setContent {
|
|
175
|
+
cv.setBackgroundColor(android.graphics.Color.TRANSPARENT)
|
|
176
|
+
cv.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
|
177
|
+
cv.setContent {
|
|
95
178
|
val value = viewModel.value.collectAsState().value
|
|
96
179
|
val blurTick by blurSignal.collectAsState()
|
|
97
180
|
val focusTick by focusSignal.collectAsState()
|
|
98
181
|
val focusManager = LocalFocusManager.current
|
|
99
182
|
val focusRequester = remember { FocusRequester() }
|
|
100
183
|
|
|
101
|
-
// при каждом blurTick снимаем фокус в compose
|
|
102
184
|
LaunchedEffect(blurTick) {
|
|
103
185
|
if (blurTick > 0) {
|
|
104
186
|
focusManager.clearFocus(force = true)
|
|
105
187
|
}
|
|
106
188
|
}
|
|
107
189
|
|
|
108
|
-
// при каждом focusTick запрашиваем фокус в compose
|
|
109
190
|
LaunchedEffect(focusTick) {
|
|
110
191
|
if (focusTick > 0) {
|
|
111
192
|
focusRequester.requestFocus()
|
|
@@ -124,7 +205,7 @@ class ControlledInputView : LinearLayout, LifecycleOwner {
|
|
|
124
205
|
returnKeyType = viewModel.returnKeyType,
|
|
125
206
|
onTextChange = { value ->
|
|
126
207
|
val surfaceId = UIManagerHelper.getSurfaceId(context)
|
|
127
|
-
val viewId = this.id
|
|
208
|
+
val viewId = this@ControlledInputView.id
|
|
128
209
|
UIManagerHelper
|
|
129
210
|
.getEventDispatcherForReactTag(context as ReactContext, viewId)
|
|
130
211
|
?.dispatchEvent(
|
|
@@ -137,7 +218,7 @@ class ControlledInputView : LinearLayout, LifecycleOwner {
|
|
|
137
218
|
},
|
|
138
219
|
onFocus = {
|
|
139
220
|
val surfaceId = UIManagerHelper.getSurfaceId(context)
|
|
140
|
-
val viewId = this.id
|
|
221
|
+
val viewId = this@ControlledInputView.id
|
|
141
222
|
UIManagerHelper
|
|
142
223
|
.getEventDispatcherForReactTag(context as ReactContext, viewId)
|
|
143
224
|
?.dispatchEvent(
|
|
@@ -149,7 +230,7 @@ class ControlledInputView : LinearLayout, LifecycleOwner {
|
|
|
149
230
|
},
|
|
150
231
|
onBlur = {
|
|
151
232
|
val surfaceId = UIManagerHelper.getSurfaceId(context)
|
|
152
|
-
val viewId = this.id
|
|
233
|
+
val viewId = this@ControlledInputView.id
|
|
153
234
|
UIManagerHelper
|
|
154
235
|
.getEventDispatcherForReactTag(context as ReactContext, viewId)
|
|
155
236
|
?.dispatchEvent(
|
|
@@ -162,8 +243,7 @@ class ControlledInputView : LinearLayout, LifecycleOwner {
|
|
|
162
243
|
focusRequester = focusRequester
|
|
163
244
|
)
|
|
164
245
|
}
|
|
165
|
-
addView(
|
|
166
|
-
|
|
246
|
+
addView(cv)
|
|
167
247
|
}
|
|
168
248
|
}
|
|
169
|
-
}
|
|
249
|
+
}
|
package/package.json
CHANGED