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.
@@ -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.content.ContextWrapper
6
- import androidx.appcompat.app.AppCompatActivity
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(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
41
- // 确保 View 卸载时清理 Compose 资源
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
- clipChildren = false
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
- // 核心修复:查找 Activity 并强行注入 Lifecycle
55
- val activity = context.lookupActivity()
52
+ // 1. 保底注入 Lifecycle
53
+ val activity = appContext.currentActivity
56
54
  if (activity != null) {
57
- if (composeView.findViewTreeLifecycleOwner() == null) {
58
- composeView.setViewTreeLifecycleOwner(activity)
59
- }
60
- if (composeView.findViewTreeSavedStateRegistryOwner() == null) {
61
- composeView.setViewTreeSavedStateRegistryOwner(activity)
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
- if (!isContentSet) {
66
- composeView.setContent {
67
- SegmentedControl(
68
- items = itemsState.value,
69
- selectedIndex = selectedIndexState.intValue,
70
- onValueChange = { index ->
71
- selectedIndexState.intValue = index
72
- val title = itemsState.value.getOrNull(index)?.title ?: ""
73
- onValueChange(mapOf("value" to title, "index" to index))
74
- },
75
- modifier = Modifier.fillMaxSize(),
76
- activeColor = activeColorState.value,
77
- ctrlBackgroundColor = ctrlBgColorState.value,
78
- textColor = textColorState.value,
79
- autoWidth = autoWidthState.value,
80
- hapticEnabled = hapticEnabledState.value,
81
- enabled = enabledState.value,
82
- glassExpansion = Config.Common.Glass.DEFAULT_EXPANSION,
83
- )
84
- }
85
- isContentSet = true
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
- // 辅助扩展函数:从 ContextWrapper 中剥离出 Activity
91
- // (虽然代码重复了,但这样解耦,避免跨包依赖问题)
92
- private fun Context.lookupActivity(): AppCompatActivity? {
93
- var ctx = this
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
- ctx = ctx.baseContext
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.content.ContextWrapper
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
- private var isContentSet = false
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
- val activity = context.lookupActivity()
84
+ // 1. 再次尝试获取 Activity (为了保底)
85
+ val activity = appContext.currentActivity
82
86
  if (activity != null) {
83
- if (composeView.findViewTreeLifecycleOwner() == null) {
84
- composeView.setViewTreeLifecycleOwner(activity)
85
- }
86
- if (composeView.findViewTreeSavedStateRegistryOwner() == null) {
87
- composeView.setViewTreeSavedStateRegistryOwner(activity)
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
- if (!isContentSet) {
92
- composeView.setContent {
93
- WaterViewEntry(
94
- childContainer = childContainer,
95
- props = props,
96
- commandFlow = commandChannel.receiveAsFlow(),
97
- onMoveDispatch = { x, y -> onMove(mapOf("x" to x, "y" to y)) },
98
- onPressInDispatch = { onPressIn(mapOf()) },
99
- onPressOutDispatch = { onPressOut(mapOf()) }
100
- )
101
- }
102
- isContentSet = true
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) super.addView(child, index)
108
- else childContainer.addView(child)
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) super.removeView(view)
113
- else childContainer.removeView(view)
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
- super.removeViewAt(index)
166
+ // 忽略
122
167
  }
123
168
  } else {
124
- super.removeViewAt(index)
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
- // 辅助扩展函数:从 ContextWrapper 中剥离出 Activity
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "golia-expo-utils",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "expo utils provided by golia.jp",
5
5
  "main": "build/index.js",
6
6
  "homepage": "https://expo.golia.jp/golia-expo-utils",