golia-expo-utils 1.0.2 → 1.0.3
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/src/main/java/expo/modules/goliaexpoutils/segmentedControl/ReactSegmentedControl.kt
CHANGED
|
@@ -2,8 +2,8 @@ package expo.modules.goliaexpoutils.segmentedControl
|
|
|
2
2
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
4
|
import android.content.Context
|
|
5
|
-
import android.
|
|
6
|
-
import
|
|
5
|
+
import android.util.Log
|
|
6
|
+
import android.view.ViewGroup
|
|
7
7
|
import androidx.compose.foundation.layout.fillMaxSize
|
|
8
8
|
import androidx.compose.runtime.mutableIntStateOf
|
|
9
9
|
import androidx.compose.runtime.mutableStateOf
|
|
@@ -34,68 +34,73 @@ class ReactSegmentedControl(context: Context, appContext: AppContext) :
|
|
|
34
34
|
val hapticEnabledState = mutableStateOf(true)
|
|
35
35
|
val enabledState = mutableStateOf(true)
|
|
36
36
|
|
|
37
|
-
private var isContentSet = false
|
|
38
|
-
|
|
39
37
|
private val composeView = ComposeView(context).apply {
|
|
40
|
-
layoutParams = LayoutParams(
|
|
41
|
-
|
|
38
|
+
layoutParams = ViewGroup.LayoutParams(
|
|
39
|
+
LayoutParams.MATCH_PARENT,
|
|
40
|
+
LayoutParams.MATCH_PARENT
|
|
41
|
+
)
|
|
42
42
|
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnDetachedFromWindow)
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
init {
|
|
46
|
-
|
|
47
|
-
clipToPadding = false
|
|
48
|
-
addView(composeView)
|
|
46
|
+
// ❌ 不要在这里 addView(composeView)
|
|
49
47
|
}
|
|
50
48
|
|
|
51
49
|
override fun onAttachedToWindow() {
|
|
52
50
|
super.onAttachedToWindow()
|
|
53
51
|
|
|
54
|
-
//
|
|
55
|
-
val activity =
|
|
52
|
+
// 1. 保底注入 Lifecycle
|
|
53
|
+
val activity = appContext.currentActivity
|
|
56
54
|
if (activity != null) {
|
|
57
|
-
|
|
58
|
-
composeView.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
composeView.
|
|
55
|
+
try {
|
|
56
|
+
if (composeView.findViewTreeLifecycleOwner() == null && activity is androidx.lifecycle.LifecycleOwner) {
|
|
57
|
+
composeView.setViewTreeLifecycleOwner(activity)
|
|
58
|
+
}
|
|
59
|
+
if (composeView.findViewTreeSavedStateRegistryOwner() == null && activity is androidx.savedstate.SavedStateRegistryOwner) {
|
|
60
|
+
composeView.setViewTreeSavedStateRegistryOwner(activity)
|
|
61
|
+
}
|
|
62
|
+
} catch (e: Exception) {
|
|
63
|
+
Log.e("ReactSegmentedControl", "Lifecycle injection failed", e)
|
|
62
64
|
}
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
67
|
+
// 2. 动态挂载
|
|
68
|
+
if (composeView.parent == null) {
|
|
69
|
+
addView(composeView)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 3. 设置内容
|
|
73
|
+
composeView.setContent {
|
|
74
|
+
SegmentedControl(
|
|
75
|
+
items = itemsState.value,
|
|
76
|
+
selectedIndex = selectedIndexState.intValue,
|
|
77
|
+
onValueChange = { index ->
|
|
78
|
+
selectedIndexState.intValue = index
|
|
79
|
+
val title = itemsState.value.getOrNull(index)?.title ?: ""
|
|
80
|
+
if (appContext.reactContext !== null) {
|
|
81
|
+
try {
|
|
82
|
+
onValueChange(mapOf("value" to title, "index" to index))
|
|
83
|
+
} catch (_: Throwable) {
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
modifier = Modifier.fillMaxSize(),
|
|
88
|
+
activeColor = activeColorState.value,
|
|
89
|
+
ctrlBackgroundColor = ctrlBgColorState.value,
|
|
90
|
+
textColor = textColorState.value,
|
|
91
|
+
autoWidth = autoWidthState.value,
|
|
92
|
+
hapticEnabled = hapticEnabledState.value,
|
|
93
|
+
enabled = enabledState.value,
|
|
94
|
+
glassExpansion = Config.Common.Glass.DEFAULT_EXPANSION,
|
|
95
|
+
)
|
|
86
96
|
}
|
|
87
97
|
}
|
|
88
|
-
}
|
|
89
98
|
|
|
90
|
-
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
while (ctx is ContextWrapper) {
|
|
95
|
-
if (ctx is AppCompatActivity) {
|
|
96
|
-
return ctx
|
|
99
|
+
override fun onDetachedFromWindow() {
|
|
100
|
+
// 4. 动态卸载
|
|
101
|
+
if (composeView.parent == this) {
|
|
102
|
+
removeView(composeView)
|
|
97
103
|
}
|
|
98
|
-
|
|
104
|
+
super.onDetachedFromWindow()
|
|
99
105
|
}
|
|
100
|
-
return null
|
|
101
106
|
}
|
|
@@ -2,11 +2,10 @@ package expo.modules.goliaexpoutils.waterView
|
|
|
2
2
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
4
|
import android.content.Context
|
|
5
|
-
import android.
|
|
5
|
+
import android.util.Log
|
|
6
6
|
import android.view.View
|
|
7
7
|
import android.view.ViewGroup
|
|
8
8
|
import android.widget.FrameLayout
|
|
9
|
-
import androidx.appcompat.app.AppCompatActivity
|
|
10
9
|
import androidx.compose.foundation.layout.Box
|
|
11
10
|
import androidx.compose.foundation.layout.fillMaxSize
|
|
12
11
|
import androidx.compose.runtime.Composable
|
|
@@ -58,70 +57,117 @@ class ReactWaterView(context: Context, appContext: AppContext) : ExpoView(contex
|
|
|
58
57
|
private val onPressIn by EventDispatcher()
|
|
59
58
|
private val onPressOut by EventDispatcher()
|
|
60
59
|
private val commandChannel = Channel<WaterCommand>(Channel.BUFFERED)
|
|
60
|
+
private val TAG = "ReactWaterView"
|
|
61
|
+
|
|
62
|
+
// 仅仅作为容器,初始化时不添加 ComposeView
|
|
61
63
|
private val childContainer = FrameLayout(context).apply {
|
|
62
64
|
layoutParams = ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
// ComposeView 懒加载,并且只在 Attached 状态下存在于 ViewTree 中
|
|
67
68
|
private val composeView = ComposeView(context).apply {
|
|
68
69
|
layoutParams = ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
|
70
|
+
// 关键策略:Detach 时立即销毁 Composition,防止内存泄漏和重绘崩溃
|
|
69
71
|
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnDetachedFromWindow)
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
init {
|
|
73
75
|
clipChildren = false
|
|
74
76
|
clipToPadding = false
|
|
75
|
-
addView(composeView)
|
|
77
|
+
// ❌ 以前这里有 addView(composeView),现在删掉!
|
|
78
|
+
// 保持 View 树干净,Fabric 测量时不会触发 Compose 逻辑
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
override fun onAttachedToWindow() {
|
|
79
82
|
super.onAttachedToWindow()
|
|
80
83
|
|
|
81
|
-
|
|
84
|
+
// 1. 再次尝试获取 Activity (为了保底)
|
|
85
|
+
val activity = appContext.currentActivity
|
|
82
86
|
if (activity != null) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
// 2. 注入 Lifecycle (如果 ReactActivity 还没给它的话)
|
|
88
|
+
try {
|
|
89
|
+
if (composeView.findViewTreeLifecycleOwner() == null && activity is androidx.lifecycle.LifecycleOwner) {
|
|
90
|
+
composeView.setViewTreeLifecycleOwner(activity)
|
|
91
|
+
}
|
|
92
|
+
if (composeView.findViewTreeSavedStateRegistryOwner() == null && activity is androidx.savedstate.SavedStateRegistryOwner) {
|
|
93
|
+
composeView.setViewTreeSavedStateRegistryOwner(activity)
|
|
94
|
+
}
|
|
95
|
+
} catch (e: Exception) {
|
|
96
|
+
Log.e(TAG, "Lifecycle injection failed", e)
|
|
88
97
|
}
|
|
89
98
|
}
|
|
90
99
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
// 3. ✅ 核心修复:只有在真正 Attach 到窗口时,才把 ComposeView 加进去
|
|
101
|
+
// 这样 Fabric 在后台测量这个 View 时,它只是一个空的 ViewGroup,不会触发 Compose
|
|
102
|
+
if (composeView.parent == null) {
|
|
103
|
+
addView(composeView)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 4. 设置内容 (Compose 会自己判断是否需要重新 Composition)
|
|
107
|
+
composeView.setContent {
|
|
108
|
+
WaterViewEntry(
|
|
109
|
+
childContainer = childContainer,
|
|
110
|
+
props = props,
|
|
111
|
+
commandFlow = commandChannel.receiveAsFlow(),
|
|
112
|
+
onMoveDispatch = { x, y ->
|
|
113
|
+
if (appContext.reactContext !== null) {
|
|
114
|
+
try {
|
|
115
|
+
onMove(mapOf("x" to x, "y" to y))
|
|
116
|
+
} catch (_: Throwable) {
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
onPressInDispatch = { onPressIn(mapOf()) },
|
|
121
|
+
onPressOutDispatch = { onPressOut(mapOf()) }
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
override fun onDetachedFromWindow() {
|
|
127
|
+
// 5. ✅ 核心修复:Detach 时立即移除 ComposeView
|
|
128
|
+
// 确保 View 被回收或离屏测量时,ComposeView 不在 ViewTree 里
|
|
129
|
+
if (composeView.parent == this) {
|
|
130
|
+
removeView(composeView)
|
|
103
131
|
}
|
|
132
|
+
super.onDetachedFromWindow()
|
|
104
133
|
}
|
|
105
134
|
|
|
135
|
+
// 下面处理 RN 子 View 的挂载 (Keep standard logic)
|
|
106
136
|
override fun addView(child: View?, index: Int) {
|
|
107
|
-
if (child == composeView)
|
|
108
|
-
|
|
137
|
+
if (child == composeView) {
|
|
138
|
+
super.addView(child, index)
|
|
139
|
+
} else {
|
|
140
|
+
// RN 的子 View (比如 Text/Image) 放到 childContainer 里
|
|
141
|
+
// 注意:childContainer 需要被 NativeChildrenContainer (AndroidView) 渲染
|
|
142
|
+
// 这一步逻辑通过 WaterViewEntry -> NativeChildrenContainer 实现
|
|
143
|
+
if (child != null) {
|
|
144
|
+
// 如果 child 已经有父节点,先移除 (Fabric 有时会重用 View)
|
|
145
|
+
(child.parent as? ViewGroup)?.removeView(child)
|
|
146
|
+
childContainer.addView(child)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
109
149
|
}
|
|
110
150
|
|
|
111
151
|
override fun removeView(view: View?) {
|
|
112
|
-
if (view == composeView)
|
|
113
|
-
|
|
152
|
+
if (view == composeView) {
|
|
153
|
+
super.removeView(view)
|
|
154
|
+
} else {
|
|
155
|
+
childContainer.removeView(view)
|
|
156
|
+
}
|
|
114
157
|
}
|
|
115
158
|
|
|
116
159
|
override fun removeViewAt(index: Int) {
|
|
160
|
+
// 这是一个 tricky 的 override,因为 RN 有时候通过 index 删除
|
|
161
|
+
// 我们要确保删的是 childContainer 里的东西,而不是把 composeView 删了
|
|
117
162
|
if (childContainer.isNotEmpty()) {
|
|
118
163
|
try {
|
|
119
|
-
childContainer.removeViewAt(0)
|
|
164
|
+
childContainer.removeViewAt(0) // 通常 RN 只是顺序删除
|
|
120
165
|
} catch (_: Exception) {
|
|
121
|
-
|
|
166
|
+
// 忽略
|
|
122
167
|
}
|
|
123
168
|
} else {
|
|
124
|
-
|
|
169
|
+
// 如果 childContainer 空了,那可能是在操作 ComposeView,虽然 RN 不应该直接操作它
|
|
170
|
+
// 保持 safe
|
|
125
171
|
}
|
|
126
172
|
}
|
|
127
173
|
|
|
@@ -130,18 +176,7 @@ class ReactWaterView(context: Context, appContext: AppContext) : ExpoView(contex
|
|
|
130
176
|
}
|
|
131
177
|
}
|
|
132
178
|
|
|
133
|
-
//
|
|
134
|
-
private fun Context.lookupActivity(): AppCompatActivity? {
|
|
135
|
-
var ctx = this
|
|
136
|
-
while (ctx is ContextWrapper) {
|
|
137
|
-
if (ctx is AppCompatActivity) {
|
|
138
|
-
return ctx
|
|
139
|
-
}
|
|
140
|
-
ctx = ctx.baseContext
|
|
141
|
-
}
|
|
142
|
-
return null
|
|
143
|
-
}
|
|
144
|
-
|
|
179
|
+
// Composable 部分保持不变 (WaterViewEntry, NativeChildrenContainer)
|
|
145
180
|
@Composable
|
|
146
181
|
private fun WaterViewEntry(
|
|
147
182
|
childContainer: FrameLayout,
|