react-native-platform-components 0.6.1 → 0.8.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/README.md +259 -44
- package/android/src/main/java/com/platformcomponents/PCLiquidGlassView.kt +84 -0
- package/android/src/main/java/com/platformcomponents/PCLiquidGlassViewManager.kt +52 -0
- package/android/src/main/java/com/platformcomponents/PCSegmentedControlView.kt +241 -0
- package/android/src/main/java/com/platformcomponents/PCSegmentedControlViewManager.kt +105 -0
- package/android/src/main/java/com/platformcomponents/PlatformComponentsPackage.kt +2 -0
- package/ios/PCDatePickerView.swift +16 -13
- package/ios/PCLiquidGlass.h +10 -0
- package/ios/PCLiquidGlass.mm +140 -0
- package/ios/PCLiquidGlass.swift +354 -0
- package/ios/PCSegmentedControl.h +10 -0
- package/ios/PCSegmentedControl.mm +194 -0
- package/ios/PCSegmentedControl.swift +200 -0
- package/ios/PCSelectionMenu.swift +1 -1
- package/lib/commonjs/LiquidGlass.js +72 -0
- package/lib/commonjs/LiquidGlass.js.map +1 -0
- package/lib/commonjs/LiquidGlassNativeComponent.ts +110 -0
- package/lib/commonjs/SegmentedControl.js +93 -0
- package/lib/commonjs/SegmentedControl.js.map +1 -0
- package/lib/commonjs/SegmentedControlNativeComponent.ts +79 -0
- package/lib/commonjs/index.js +22 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/LiquidGlass.js +64 -0
- package/lib/module/LiquidGlass.js.map +1 -0
- package/lib/module/LiquidGlassNativeComponent.ts +110 -0
- package/lib/module/SegmentedControl.js +87 -0
- package/lib/module/SegmentedControl.js.map +1 -0
- package/lib/module/SegmentedControlNativeComponent.ts +79 -0
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/commonjs/src/LiquidGlass.d.ts +96 -0
- package/lib/typescript/commonjs/src/LiquidGlass.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/LiquidGlassNativeComponent.d.ts +93 -0
- package/lib/typescript/commonjs/src/LiquidGlassNativeComponent.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/SegmentedControl.d.ts +62 -0
- package/lib/typescript/commonjs/src/SegmentedControl.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/SegmentedControlNativeComponent.d.ts +63 -0
- package/lib/typescript/commonjs/src/SegmentedControlNativeComponent.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/index.d.ts +2 -0
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
- package/lib/typescript/module/src/LiquidGlass.d.ts +96 -0
- package/lib/typescript/module/src/LiquidGlass.d.ts.map +1 -0
- package/lib/typescript/module/src/LiquidGlassNativeComponent.d.ts +93 -0
- package/lib/typescript/module/src/LiquidGlassNativeComponent.d.ts.map +1 -0
- package/lib/typescript/module/src/SegmentedControl.d.ts +62 -0
- package/lib/typescript/module/src/SegmentedControl.d.ts.map +1 -0
- package/lib/typescript/module/src/SegmentedControlNativeComponent.d.ts +63 -0
- package/lib/typescript/module/src/SegmentedControlNativeComponent.d.ts.map +1 -0
- package/lib/typescript/module/src/index.d.ts +2 -0
- package/lib/typescript/module/src/index.d.ts.map +1 -1
- package/package.json +13 -4
- package/react-native.config.js +1 -0
- package/shared/PCSegmentedControlComponentDescriptors-custom.h +22 -0
- package/shared/PCSegmentedControlShadowNode-custom.cpp +54 -0
- package/shared/PCSegmentedControlShadowNode-custom.h +56 -0
- package/shared/PCSegmentedControlState-custom.h +62 -0
- package/shared/react/renderer/components/PlatformComponentsViewSpec/ComponentDescriptors.h +1 -0
- package/src/LiquidGlass.tsx +169 -0
- package/src/LiquidGlassNativeComponent.ts +110 -0
- package/src/SegmentedControl.tsx +178 -0
- package/src/SegmentedControlNativeComponent.ts +79 -0
- package/src/index.tsx +2 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
package com.platformcomponents
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.text.TextUtils
|
|
5
|
+
import android.view.View
|
|
6
|
+
import android.widget.FrameLayout
|
|
7
|
+
import com.facebook.react.bridge.WritableNativeMap
|
|
8
|
+
import com.facebook.react.uimanager.PixelUtil
|
|
9
|
+
import com.facebook.react.uimanager.StateWrapper
|
|
10
|
+
import com.facebook.react.views.scroll.ReactScrollViewHelper
|
|
11
|
+
import com.google.android.material.button.MaterialButton
|
|
12
|
+
import com.google.android.material.button.MaterialButtonToggleGroup
|
|
13
|
+
|
|
14
|
+
class PCSegmentedControlView(context: Context) : FrameLayout(context), ReactScrollViewHelper.HasStateWrapper {
|
|
15
|
+
|
|
16
|
+
data class Segment(
|
|
17
|
+
val label: String,
|
|
18
|
+
val value: String,
|
|
19
|
+
val disabled: Boolean,
|
|
20
|
+
val icon: String
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
companion object {
|
|
24
|
+
private const val TAG = "PCSegmentedControl"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// --- State Wrapper for Fabric state updates ---
|
|
28
|
+
override var stateWrapper: StateWrapper? = null
|
|
29
|
+
|
|
30
|
+
private var lastReportedWidth: Float = 0f
|
|
31
|
+
private var lastReportedHeight: Float = 0f
|
|
32
|
+
|
|
33
|
+
// --- Props ---
|
|
34
|
+
var segments: List<Segment> = emptyList()
|
|
35
|
+
var selectedValue: String = "" // sentinel for none
|
|
36
|
+
var interactivity: String = "enabled" // "enabled" | "disabled"
|
|
37
|
+
var selectionRequired: Boolean = false
|
|
38
|
+
|
|
39
|
+
// --- Events ---
|
|
40
|
+
var onSelect: ((index: Int, value: String) -> Unit)? = null
|
|
41
|
+
|
|
42
|
+
// --- UI ---
|
|
43
|
+
private var toggleGroup: MaterialButtonToggleGroup? = null
|
|
44
|
+
private val buttonIdToSegment: MutableMap<Int, Segment> = mutableMapOf()
|
|
45
|
+
private var suppressCallbacks = false
|
|
46
|
+
|
|
47
|
+
init {
|
|
48
|
+
minimumHeight = (PCConstants.MIN_TOUCH_TARGET_HEIGHT_DP * resources.displayMetrics.density).toInt()
|
|
49
|
+
rebuildUI()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---- Public apply* (called by manager) ----
|
|
53
|
+
|
|
54
|
+
fun applySegments(newSegments: List<Segment>) {
|
|
55
|
+
if (segments == newSegments) return
|
|
56
|
+
segments = newSegments
|
|
57
|
+
rebuildUI()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fun applySelectedValue(value: String) {
|
|
61
|
+
if (selectedValue == value) return
|
|
62
|
+
selectedValue = value
|
|
63
|
+
updateSelection()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fun applyInteractivity(value: String?) {
|
|
67
|
+
val newValue = if (value == "disabled") "disabled" else "enabled"
|
|
68
|
+
if (interactivity == newValue) return
|
|
69
|
+
interactivity = newValue
|
|
70
|
+
updateEnabled()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
fun applyAndroidProps(required: Boolean) {
|
|
74
|
+
if (selectionRequired != required) {
|
|
75
|
+
selectionRequired = required
|
|
76
|
+
rebuildUI()
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---- UI Building ----
|
|
81
|
+
|
|
82
|
+
private fun rebuildUI() {
|
|
83
|
+
removeAllViews()
|
|
84
|
+
buttonIdToSegment.clear()
|
|
85
|
+
|
|
86
|
+
val group = MaterialButtonToggleGroup(context).apply {
|
|
87
|
+
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
|
88
|
+
isSingleSelection = true
|
|
89
|
+
isSelectionRequired = selectionRequired
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Calculate if we need compact mode (many segments or long labels)
|
|
93
|
+
val totalLabelLength = segments.sumOf { it.label.length }
|
|
94
|
+
val useCompactMode = segments.size > 3 || totalLabelLength > 20
|
|
95
|
+
|
|
96
|
+
for ((index, segment) in segments.withIndex()) {
|
|
97
|
+
val button = MaterialButton(context, null, com.google.android.material.R.attr.materialButtonOutlinedStyle).apply {
|
|
98
|
+
id = View.generateViewId()
|
|
99
|
+
text = segment.label
|
|
100
|
+
isAllCaps = false // Preserve original text casing
|
|
101
|
+
contentDescription = segment.label // For accessibility and Detox matching
|
|
102
|
+
isEnabled = !segment.disabled && interactivity == "enabled"
|
|
103
|
+
|
|
104
|
+
// Enable text truncation with ellipsis when space is limited
|
|
105
|
+
ellipsize = TextUtils.TruncateAt.END
|
|
106
|
+
maxLines = 1
|
|
107
|
+
|
|
108
|
+
// Reduce horizontal padding in compact mode to fit more content
|
|
109
|
+
if (useCompactMode) {
|
|
110
|
+
val compactPadding = (8 * resources.displayMetrics.density).toInt()
|
|
111
|
+
setPaddingRelative(compactPadding, paddingTop, compactPadding, paddingBottom)
|
|
112
|
+
iconPadding = (4 * resources.displayMetrics.density).toInt()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Set icon if available
|
|
116
|
+
if (segment.icon.isNotEmpty()) {
|
|
117
|
+
val resId = context.resources.getIdentifier(
|
|
118
|
+
segment.icon, "drawable", context.packageName
|
|
119
|
+
)
|
|
120
|
+
if (resId != 0) {
|
|
121
|
+
setIconResource(resId)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Handle click to trigger selection (needed for Detox taps)
|
|
126
|
+
setOnClickListener {
|
|
127
|
+
if (!suppressCallbacks && isEnabled) {
|
|
128
|
+
group.check(id)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
buttonIdToSegment[button.id] = segment
|
|
134
|
+
group.addView(button)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
group.addOnButtonCheckedListener { _, checkedId, isChecked ->
|
|
138
|
+
if (suppressCallbacks) return@addOnButtonCheckedListener
|
|
139
|
+
if (!isChecked) return@addOnButtonCheckedListener
|
|
140
|
+
|
|
141
|
+
val segment = buttonIdToSegment[checkedId] ?: return@addOnButtonCheckedListener
|
|
142
|
+
val index = segments.indexOf(segment)
|
|
143
|
+
if (index >= 0) {
|
|
144
|
+
onSelect?.invoke(index, segment.value)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
addView(group)
|
|
149
|
+
toggleGroup = group
|
|
150
|
+
|
|
151
|
+
updateSelection()
|
|
152
|
+
updateEnabled()
|
|
153
|
+
requestLayout()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private fun updateSelection() {
|
|
157
|
+
suppressCallbacks = true
|
|
158
|
+
val group = toggleGroup ?: return
|
|
159
|
+
|
|
160
|
+
if (selectedValue.isEmpty()) {
|
|
161
|
+
group.clearChecked()
|
|
162
|
+
} else {
|
|
163
|
+
for ((id, segment) in buttonIdToSegment) {
|
|
164
|
+
if (segment.value == selectedValue) {
|
|
165
|
+
group.check(id)
|
|
166
|
+
break
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
suppressCallbacks = false
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private fun updateEnabled() {
|
|
174
|
+
val enabled = interactivity == "enabled"
|
|
175
|
+
alpha = if (enabled) 1f else 0.5f
|
|
176
|
+
|
|
177
|
+
val group = toggleGroup ?: return
|
|
178
|
+
for (i in 0 until group.childCount) {
|
|
179
|
+
val button = group.getChildAt(i) as? MaterialButton ?: continue
|
|
180
|
+
val segment = buttonIdToSegment[button.id] ?: continue
|
|
181
|
+
button.isEnabled = enabled && !segment.disabled
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ---- Measurement ----
|
|
186
|
+
|
|
187
|
+
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
188
|
+
// Measure children with UNSPECIFIED height to get intrinsic size
|
|
189
|
+
val unconstrainedHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
|
|
190
|
+
super.onMeasure(widthMeasureSpec, unconstrainedHeightSpec)
|
|
191
|
+
|
|
192
|
+
// Get the intrinsic height from children
|
|
193
|
+
val childHeight = if (toggleGroup != null) {
|
|
194
|
+
toggleGroup!!.measuredHeight
|
|
195
|
+
} else {
|
|
196
|
+
0
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Use the maximum of child height and minimum touch target
|
|
200
|
+
val intrinsicHeight = childHeight.coerceAtLeast(minimumHeight)
|
|
201
|
+
|
|
202
|
+
// IMPORTANT: Always use intrinsic height regardless of Yoga constraints.
|
|
203
|
+
// Fabric may give us 0 height initially before state is updated.
|
|
204
|
+
// The state update from onLayout will trigger proper re-layout.
|
|
205
|
+
setMeasuredDimension(measuredWidth, intrinsicHeight)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
209
|
+
super.onLayout(changed, left, top, right, bottom)
|
|
210
|
+
updateFrameSizeState()
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Update Fabric state with the measured frame size.
|
|
215
|
+
* This allows the shadow node to use actual measured dimensions for Yoga layout.
|
|
216
|
+
*/
|
|
217
|
+
private fun updateFrameSizeState() {
|
|
218
|
+
val wrapper = stateWrapper ?: return
|
|
219
|
+
val group = toggleGroup ?: return
|
|
220
|
+
|
|
221
|
+
// Measure the toggle group with exact width and unspecified height
|
|
222
|
+
val widthSpec = MeasureSpec.makeMeasureSpec(width.coerceAtLeast(1), MeasureSpec.EXACTLY)
|
|
223
|
+
val heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
|
|
224
|
+
group.measure(widthSpec, heightSpec)
|
|
225
|
+
|
|
226
|
+
val widthDp = PixelUtil.toDIPFromPixel(width.toFloat())
|
|
227
|
+
val heightDp = PixelUtil.toDIPFromPixel(group.measuredHeight.toFloat())
|
|
228
|
+
|
|
229
|
+
// Only update if changed
|
|
230
|
+
if (widthDp != lastReportedWidth || heightDp != lastReportedHeight) {
|
|
231
|
+
lastReportedWidth = widthDp
|
|
232
|
+
lastReportedHeight = heightDp
|
|
233
|
+
|
|
234
|
+
val stateData = WritableNativeMap().apply {
|
|
235
|
+
putDouble("width", widthDp.toDouble())
|
|
236
|
+
putDouble("height", heightDp.toDouble())
|
|
237
|
+
}
|
|
238
|
+
wrapper.updateState(stateData)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
package com.platformcomponents
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.ReadableArray
|
|
4
|
+
import com.facebook.react.bridge.ReadableMap
|
|
5
|
+
import com.facebook.react.uimanager.ReactStylesDiffMap
|
|
6
|
+
import com.facebook.react.uimanager.SimpleViewManager
|
|
7
|
+
import com.facebook.react.uimanager.StateWrapper
|
|
8
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
9
|
+
import com.facebook.react.uimanager.ViewManagerDelegate
|
|
10
|
+
import com.facebook.react.uimanager.UIManagerHelper
|
|
11
|
+
import com.facebook.react.uimanager.events.Event
|
|
12
|
+
import com.facebook.react.uimanager.events.RCTEventEmitter
|
|
13
|
+
import com.facebook.react.viewmanagers.PCSegmentedControlManagerDelegate
|
|
14
|
+
import com.facebook.react.viewmanagers.PCSegmentedControlManagerInterface
|
|
15
|
+
|
|
16
|
+
class PCSegmentedControlViewManager :
|
|
17
|
+
SimpleViewManager<PCSegmentedControlView>(),
|
|
18
|
+
PCSegmentedControlManagerInterface<PCSegmentedControlView> {
|
|
19
|
+
|
|
20
|
+
companion object {
|
|
21
|
+
private const val TAG = "PCSegmentedControl"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private val delegate: ViewManagerDelegate<PCSegmentedControlView> =
|
|
25
|
+
PCSegmentedControlManagerDelegate(this)
|
|
26
|
+
|
|
27
|
+
override fun getName(): String = "PCSegmentedControl"
|
|
28
|
+
|
|
29
|
+
override fun getDelegate(): ViewManagerDelegate<PCSegmentedControlView> = delegate
|
|
30
|
+
|
|
31
|
+
override fun createViewInstance(reactContext: ThemedReactContext): PCSegmentedControlView {
|
|
32
|
+
return PCSegmentedControlView(reactContext)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Pass the StateWrapper to the view so it can update Fabric state with measured dimensions.
|
|
37
|
+
*/
|
|
38
|
+
override fun updateState(
|
|
39
|
+
view: PCSegmentedControlView,
|
|
40
|
+
props: ReactStylesDiffMap,
|
|
41
|
+
stateWrapper: StateWrapper
|
|
42
|
+
): Any? {
|
|
43
|
+
view.stateWrapper = stateWrapper
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
override fun addEventEmitters(reactContext: ThemedReactContext, view: PCSegmentedControlView) {
|
|
48
|
+
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.id)
|
|
49
|
+
|
|
50
|
+
view.onSelect = { index, value ->
|
|
51
|
+
dispatcher?.dispatchEvent(SelectEvent(view.id, index, value))
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// segments: array of {label, value, disabled, icon}
|
|
56
|
+
override fun setSegments(view: PCSegmentedControlView, value: ReadableArray?) {
|
|
57
|
+
val out = ArrayList<PCSegmentedControlView.Segment>()
|
|
58
|
+
if (value != null) {
|
|
59
|
+
for (i in 0 until value.size()) {
|
|
60
|
+
val m = value.getMap(i) ?: continue
|
|
61
|
+
val label = if (m.hasKey("label") && !m.isNull("label")) m.getString("label") ?: "" else ""
|
|
62
|
+
val segValue = if (m.hasKey("value") && !m.isNull("value")) m.getString("value") ?: "" else ""
|
|
63
|
+
val disabled = m.hasKey("disabled") && !m.isNull("disabled") && m.getString("disabled") == "disabled"
|
|
64
|
+
val icon = if (m.hasKey("icon") && !m.isNull("icon")) m.getString("icon") ?: "" else ""
|
|
65
|
+
out.add(PCSegmentedControlView.Segment(label = label, value = segValue, disabled = disabled, icon = icon))
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
view.applySegments(out)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
override fun setSelectedValue(view: PCSegmentedControlView, value: String?) {
|
|
72
|
+
// Spec sentinel: empty string means "no selection"
|
|
73
|
+
view.applySelectedValue(value ?: "")
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
override fun setInteractivity(view: PCSegmentedControlView, value: String?) {
|
|
77
|
+
view.applyInteractivity(value)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
override fun setAndroid(view: PCSegmentedControlView, value: ReadableMap?) {
|
|
81
|
+
val selectionRequired = value != null && value.hasKey("selectionRequired") &&
|
|
82
|
+
!value.isNull("selectionRequired") && value.getString("selectionRequired") == "true"
|
|
83
|
+
view.applyAndroidProps(selectionRequired)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
override fun setIos(view: PCSegmentedControlView, value: ReadableMap?) {
|
|
87
|
+
// Android ignores iOS config
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// --- Events ---
|
|
91
|
+
private class SelectEvent(
|
|
92
|
+
surfaceId: Int,
|
|
93
|
+
private val index: Int,
|
|
94
|
+
private val value: String
|
|
95
|
+
) : Event<SelectEvent>(surfaceId) {
|
|
96
|
+
override fun getEventName(): String = "topSelect"
|
|
97
|
+
override fun dispatch(rctEventEmitter: RCTEventEmitter) {
|
|
98
|
+
val payload = com.facebook.react.bridge.Arguments.createMap().apply {
|
|
99
|
+
putInt("index", index)
|
|
100
|
+
putString("value", value)
|
|
101
|
+
}
|
|
102
|
+
rctEventEmitter.receiveEvent(viewTag, eventName, payload)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -283,30 +283,33 @@ public final class PCDatePickerView: UIControl,
|
|
|
283
283
|
// Prevent "settle" events right as we present.
|
|
284
284
|
suppressNextChangesBriefly()
|
|
285
285
|
|
|
286
|
-
// Check if using inline style (full calendar) - needs larger popover size
|
|
287
|
-
var isInlineStyle = false
|
|
288
|
-
if #available(iOS 13.4, *) {
|
|
289
|
-
isInlineStyle = picker.preferredDatePickerStyle == .inline
|
|
290
|
-
}
|
|
291
|
-
|
|
292
286
|
let vc = UIViewController()
|
|
293
287
|
picker.translatesAutoresizingMaskIntoConstraints = false
|
|
294
288
|
vc.view.addSubview(picker)
|
|
295
289
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
290
|
+
let useInlineFallback: Bool
|
|
291
|
+
if #available(iOS 26.0, *) {
|
|
292
|
+
useInlineFallback = false
|
|
293
|
+
} else {
|
|
294
|
+
useInlineFallback = true
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if useInlineFallback {
|
|
298
|
+
// Pre–Liquid Glass fallback
|
|
299
299
|
vc.view.backgroundColor = .systemBackground
|
|
300
300
|
vc.view.isOpaque = true
|
|
301
|
+
|
|
301
302
|
NSLayoutConstraint.activate([
|
|
302
|
-
picker.topAnchor.constraint(equalTo: vc.view.topAnchor
|
|
303
|
-
picker.leadingAnchor.constraint(equalTo: vc.view.leadingAnchor
|
|
304
|
-
picker.trailingAnchor.constraint(equalTo: vc.view.trailingAnchor
|
|
305
|
-
picker.bottomAnchor.constraint(equalTo: vc.view.bottomAnchor
|
|
303
|
+
picker.topAnchor.constraint(equalTo: vc.view.topAnchor),
|
|
304
|
+
picker.leadingAnchor.constraint(equalTo: vc.view.leadingAnchor),
|
|
305
|
+
picker.trailingAnchor.constraint(equalTo: vc.view.trailingAnchor),
|
|
306
|
+
picker.bottomAnchor.constraint(equalTo: vc.view.bottomAnchor),
|
|
306
307
|
])
|
|
307
308
|
} else {
|
|
309
|
+
// Liquid Glass path
|
|
308
310
|
vc.view.backgroundColor = .clear
|
|
309
311
|
vc.view.isOpaque = false
|
|
312
|
+
|
|
310
313
|
NSLayoutConstraint.activate([
|
|
311
314
|
picker.topAnchor.constraint(equalTo: vc.view.topAnchor),
|
|
312
315
|
picker.leadingAnchor.constraint(equalTo: vc.view.leadingAnchor),
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// PCLiquidGlass.mm
|
|
2
|
+
|
|
3
|
+
#import "PCLiquidGlass.h"
|
|
4
|
+
|
|
5
|
+
#import <React/RCTComponentViewFactory.h>
|
|
6
|
+
#import <React/RCTConversions.h>
|
|
7
|
+
#import <React/RCTFabricComponentsPlugins.h>
|
|
8
|
+
|
|
9
|
+
#import <react/renderer/components/PlatformComponentsViewSpec/ComponentDescriptors.h>
|
|
10
|
+
#import <react/renderer/components/PlatformComponentsViewSpec/EventEmitters.h>
|
|
11
|
+
#import <react/renderer/components/PlatformComponentsViewSpec/Props.h>
|
|
12
|
+
|
|
13
|
+
#if __has_include(<PlatformComponents/PlatformComponents-Swift.h>)
|
|
14
|
+
#import <PlatformComponents/PlatformComponents-Swift.h>
|
|
15
|
+
#else
|
|
16
|
+
#import "PlatformComponents-Swift.h"
|
|
17
|
+
#endif
|
|
18
|
+
|
|
19
|
+
using namespace facebook::react;
|
|
20
|
+
|
|
21
|
+
@implementation PCLiquidGlass {
|
|
22
|
+
PCLiquidGlassView *_view;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider {
|
|
26
|
+
return concreteComponentDescriptorProvider<PCLiquidGlassComponentDescriptor>();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
- (instancetype)initWithFrame:(CGRect)frame {
|
|
30
|
+
if (self = [super initWithFrame:frame]) {
|
|
31
|
+
_view = [[PCLiquidGlassView alloc] initWithEffect:nil];
|
|
32
|
+
self.contentView = _view;
|
|
33
|
+
|
|
34
|
+
// Set up press callback
|
|
35
|
+
__weak __typeof(self) weakSelf = self;
|
|
36
|
+
_view.onPressCallback = ^(CGFloat x, CGFloat y) {
|
|
37
|
+
__typeof(self) strongSelf = weakSelf;
|
|
38
|
+
if (!strongSelf) return;
|
|
39
|
+
|
|
40
|
+
auto eventEmitter =
|
|
41
|
+
std::static_pointer_cast<const PCLiquidGlassEventEmitter>(
|
|
42
|
+
strongSelf->_eventEmitter);
|
|
43
|
+
if (!eventEmitter) return;
|
|
44
|
+
|
|
45
|
+
PCLiquidGlassEventEmitter::OnGlassPress payload = {
|
|
46
|
+
.x = (float)x,
|
|
47
|
+
.y = (float)y,
|
|
48
|
+
};
|
|
49
|
+
eventEmitter->onGlassPress(payload);
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return self;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Mount children into the UIVisualEffectView's contentView
|
|
56
|
+
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index {
|
|
57
|
+
[_view.contentView insertSubview:childComponentView atIndex:index];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index {
|
|
61
|
+
[childComponentView removeFromSuperview];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
- (void)updateProps:(Props::Shared const &)props
|
|
65
|
+
oldProps:(Props::Shared const &)oldProps {
|
|
66
|
+
const auto &newProps =
|
|
67
|
+
*std::static_pointer_cast<const PCLiquidGlassProps>(props);
|
|
68
|
+
const auto prevProps =
|
|
69
|
+
std::static_pointer_cast<const PCLiquidGlassProps>(oldProps);
|
|
70
|
+
|
|
71
|
+
BOOL needsSetup = NO;
|
|
72
|
+
|
|
73
|
+
// cornerRadius -> glassCornerRadius
|
|
74
|
+
if (!prevProps || newProps.cornerRadius != prevProps->cornerRadius) {
|
|
75
|
+
_view.glassCornerRadius = newProps.cornerRadius;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// iOS-specific props
|
|
79
|
+
const auto &newIos = newProps.ios;
|
|
80
|
+
const auto &oldIos = prevProps ? prevProps->ios : PCLiquidGlassIosStruct{};
|
|
81
|
+
|
|
82
|
+
// interactive - needs setup to re-create effect with isInteractive
|
|
83
|
+
if (!prevProps || newIos.interactive != oldIos.interactive) {
|
|
84
|
+
_view.interactive = (newIos.interactive == "true");
|
|
85
|
+
needsSetup = YES;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// effect -> effectStyle
|
|
89
|
+
if (!prevProps || newIos.effect != oldIos.effect) {
|
|
90
|
+
if (!newIos.effect.empty()) {
|
|
91
|
+
_view.effectStyle = [NSString stringWithUTF8String:newIos.effect.c_str()];
|
|
92
|
+
} else {
|
|
93
|
+
_view.effectStyle = @"regular";
|
|
94
|
+
}
|
|
95
|
+
needsSetup = YES;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// tintColor -> glassTintColor
|
|
99
|
+
if (!prevProps || newIos.tintColor != oldIos.tintColor) {
|
|
100
|
+
if (!newIos.tintColor.empty()) {
|
|
101
|
+
_view.glassTintColor = [NSString stringWithUTF8String:newIos.tintColor.c_str()];
|
|
102
|
+
} else {
|
|
103
|
+
_view.glassTintColor = nil;
|
|
104
|
+
}
|
|
105
|
+
needsSetup = YES;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// colorScheme
|
|
109
|
+
if (!prevProps || newIos.colorScheme != oldIos.colorScheme) {
|
|
110
|
+
if (!newIos.colorScheme.empty()) {
|
|
111
|
+
_view.colorScheme = [NSString stringWithUTF8String:newIos.colorScheme.c_str()];
|
|
112
|
+
} else {
|
|
113
|
+
_view.colorScheme = @"system";
|
|
114
|
+
}
|
|
115
|
+
needsSetup = YES;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// shadowRadius -> glassShadowRadius
|
|
119
|
+
if (!prevProps || newIos.shadowRadius != oldIos.shadowRadius) {
|
|
120
|
+
_view.glassShadowRadius = newIos.shadowRadius;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// isHighlighted
|
|
124
|
+
if (!prevProps || newIos.isHighlighted != oldIos.isHighlighted) {
|
|
125
|
+
_view.isHighlighted = (newIos.isHighlighted == "true");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Apply glass effect if any glass-related props changed
|
|
129
|
+
if (needsSetup) {
|
|
130
|
+
[_view setupView];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
[super updateProps:props oldProps:oldProps];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@end
|
|
137
|
+
|
|
138
|
+
Class<RCTComponentViewProtocol> PCLiquidGlassCls(void) {
|
|
139
|
+
return PCLiquidGlass.class;
|
|
140
|
+
}
|