react-native-platform-components 1.3.0 → 1.4.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 +24 -30
- package/android/src/main/java/com/platformcomponents/PCButtonGroupView.kt +498 -0
- package/android/src/main/java/com/platformcomponents/PCButtonGroupViewManager.kt +191 -0
- package/android/src/main/java/com/platformcomponents/PCButtonSupport.kt +127 -0
- package/android/src/main/java/com/platformcomponents/PCButtonView.kt +345 -0
- package/android/src/main/java/com/platformcomponents/PCButtonViewManager.kt +126 -0
- package/android/src/main/java/com/platformcomponents/PCExpressive.kt +107 -0
- package/android/src/main/java/com/platformcomponents/PCFloatingToolbarView.kt +115 -0
- package/android/src/main/java/com/platformcomponents/PCFloatingToolbarViewManager.kt +65 -0
- package/android/src/main/java/com/platformcomponents/PCSegmentedControlView.kt +11 -3
- package/android/src/main/java/com/platformcomponents/PCSegmentedControlViewManager.kt +6 -1
- package/android/src/main/java/com/platformcomponents/PCTextFieldView.kt +1018 -0
- package/android/src/main/java/com/platformcomponents/PCTextFieldViewManager.kt +248 -0
- package/android/src/main/java/com/platformcomponents/PlatformComponentsPackage.kt +4 -0
- package/android/src/main/res/values/attrs.xml +13 -0
- package/android/src/main/res/values/styles.xml +183 -0
- package/ios/PCButton.h +10 -0
- package/ios/PCButton.mm +196 -0
- package/ios/PCButtonGroup.h +10 -0
- package/ios/PCButtonGroup.mm +256 -0
- package/ios/PCButtonGroupView.swift +254 -0
- package/ios/PCButtonSupport.swift +141 -0
- package/ios/PCButtonView.swift +155 -0
- package/ios/PCFloatingToolbar.h +10 -0
- package/ios/PCFloatingToolbar.mm +76 -0
- package/ios/PCFloatingToolbarView.swift +95 -0
- package/ios/PCTextField.h +10 -0
- package/ios/PCTextField.mm +391 -0
- package/ios/PCTextFieldView.swift +1012 -0
- package/lib/commonjs/Button.js +77 -0
- package/lib/commonjs/Button.js.map +1 -0
- package/lib/commonjs/ButtonGroup.js +99 -0
- package/lib/commonjs/ButtonGroup.js.map +1 -0
- package/lib/commonjs/ButtonGroupNativeComponent.ts +121 -0
- package/lib/commonjs/ButtonNativeComponent.ts +76 -0
- package/lib/commonjs/FloatingToolbar.js +69 -0
- package/lib/commonjs/FloatingToolbar.js.map +1 -0
- package/lib/commonjs/FloatingToolbarNativeComponent.ts +31 -0
- package/lib/commonjs/SegmentedControl.js +11 -85
- package/lib/commonjs/SegmentedControl.js.map +1 -1
- package/lib/commonjs/SegmentedControlNativeComponent.ts +3 -0
- package/lib/commonjs/TextField.js +195 -0
- package/lib/commonjs/TextField.js.map +1 -0
- package/lib/commonjs/TextFieldNativeComponent.ts +218 -0
- package/lib/commonjs/focusRegistry.js +78 -0
- package/lib/commonjs/focusRegistry.js.map +1 -0
- package/lib/commonjs/icons.js +92 -0
- package/lib/commonjs/icons.js.map +1 -0
- package/lib/commonjs/index.js +66 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +32 -2
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/labelStyle.js +27 -0
- package/lib/commonjs/labelStyle.js.map +1 -0
- package/lib/commonjs/reactNativeInternals.d.js +2 -0
- package/lib/commonjs/reactNativeInternals.d.js.map +1 -0
- package/lib/module/Button.js +70 -0
- package/lib/module/Button.js.map +1 -0
- package/lib/module/ButtonGroup.js +92 -0
- package/lib/module/ButtonGroup.js.map +1 -0
- package/lib/module/ButtonGroupNativeComponent.ts +121 -0
- package/lib/module/ButtonNativeComponent.ts +76 -0
- package/lib/module/FloatingToolbar.js +62 -0
- package/lib/module/FloatingToolbar.js.map +1 -0
- package/lib/module/FloatingToolbarNativeComponent.ts +31 -0
- package/lib/module/SegmentedControl.js +12 -87
- package/lib/module/SegmentedControl.js.map +1 -1
- package/lib/module/SegmentedControlNativeComponent.ts +3 -0
- package/lib/module/TextField.js +189 -0
- package/lib/module/TextField.js.map +1 -0
- package/lib/module/TextFieldNativeComponent.ts +218 -0
- package/lib/module/focusRegistry.js +74 -0
- package/lib/module/focusRegistry.js.map +1 -0
- package/lib/module/icons.js +87 -0
- package/lib/module/icons.js.map +1 -0
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +28 -0
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/labelStyle.js +23 -0
- package/lib/module/labelStyle.js.map +1 -0
- package/lib/module/reactNativeInternals.d.js +2 -0
- package/lib/module/reactNativeInternals.d.js.map +1 -0
- package/lib/typescript/commonjs/docs/sidebars.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/Button.d.ts +76 -0
- package/lib/typescript/commonjs/src/Button.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/ButtonGroup.d.ts +102 -0
- package/lib/typescript/commonjs/src/ButtonGroup.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/ButtonGroupNativeComponent.d.ts +93 -0
- package/lib/typescript/commonjs/src/ButtonGroupNativeComponent.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/ButtonNativeComponent.d.ts +57 -0
- package/lib/typescript/commonjs/src/ButtonNativeComponent.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/FloatingToolbar.d.ts +45 -0
- package/lib/typescript/commonjs/src/FloatingToolbar.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/FloatingToolbarNativeComponent.d.ts +24 -0
- package/lib/typescript/commonjs/src/FloatingToolbarNativeComponent.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/SegmentedControl.d.ts +17 -46
- package/lib/typescript/commonjs/src/SegmentedControl.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/SegmentedControlNativeComponent.d.ts +2 -0
- package/lib/typescript/commonjs/src/SegmentedControlNativeComponent.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/TextField.d.ts +174 -0
- package/lib/typescript/commonjs/src/TextField.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/TextFieldNativeComponent.d.ts +162 -0
- package/lib/typescript/commonjs/src/TextFieldNativeComponent.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/focusRegistry.d.ts +13 -0
- package/lib/typescript/commonjs/src/focusRegistry.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/icons.d.ts +52 -0
- package/lib/typescript/commonjs/src/icons.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/index.d.ts +6 -0
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/index.web.d.ts +121 -0
- package/lib/typescript/commonjs/src/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/labelStyle.d.ts +20 -0
- package/lib/typescript/commonjs/src/labelStyle.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/sharedTypes.d.ts +6 -1
- package/lib/typescript/commonjs/src/sharedTypes.d.ts.map +1 -1
- package/lib/typescript/module/docs/sidebars.d.ts.map +1 -1
- package/lib/typescript/module/src/Button.d.ts +76 -0
- package/lib/typescript/module/src/Button.d.ts.map +1 -0
- package/lib/typescript/module/src/ButtonGroup.d.ts +102 -0
- package/lib/typescript/module/src/ButtonGroup.d.ts.map +1 -0
- package/lib/typescript/module/src/ButtonGroupNativeComponent.d.ts +93 -0
- package/lib/typescript/module/src/ButtonGroupNativeComponent.d.ts.map +1 -0
- package/lib/typescript/module/src/ButtonNativeComponent.d.ts +57 -0
- package/lib/typescript/module/src/ButtonNativeComponent.d.ts.map +1 -0
- package/lib/typescript/module/src/FloatingToolbar.d.ts +45 -0
- package/lib/typescript/module/src/FloatingToolbar.d.ts.map +1 -0
- package/lib/typescript/module/src/FloatingToolbarNativeComponent.d.ts +24 -0
- package/lib/typescript/module/src/FloatingToolbarNativeComponent.d.ts.map +1 -0
- package/lib/typescript/module/src/SegmentedControl.d.ts +17 -46
- package/lib/typescript/module/src/SegmentedControl.d.ts.map +1 -1
- package/lib/typescript/module/src/SegmentedControlNativeComponent.d.ts +2 -0
- package/lib/typescript/module/src/SegmentedControlNativeComponent.d.ts.map +1 -1
- package/lib/typescript/module/src/TextField.d.ts +174 -0
- package/lib/typescript/module/src/TextField.d.ts.map +1 -0
- package/lib/typescript/module/src/TextFieldNativeComponent.d.ts +162 -0
- package/lib/typescript/module/src/TextFieldNativeComponent.d.ts.map +1 -0
- package/lib/typescript/module/src/focusRegistry.d.ts +13 -0
- package/lib/typescript/module/src/focusRegistry.d.ts.map +1 -0
- package/lib/typescript/module/src/icons.d.ts +52 -0
- package/lib/typescript/module/src/icons.d.ts.map +1 -0
- package/lib/typescript/module/src/index.d.ts +6 -0
- package/lib/typescript/module/src/index.d.ts.map +1 -1
- package/lib/typescript/module/src/index.web.d.ts +121 -0
- package/lib/typescript/module/src/index.web.d.ts.map +1 -1
- package/lib/typescript/module/src/labelStyle.d.ts +20 -0
- package/lib/typescript/module/src/labelStyle.d.ts.map +1 -0
- package/lib/typescript/module/src/sharedTypes.d.ts +6 -1
- package/lib/typescript/module/src/sharedTypes.d.ts.map +1 -1
- package/package.json +25 -4
- package/react-native.config.js +3 -0
- package/shared/PCButtonComponentDescriptors-custom.h +22 -0
- package/shared/PCButtonGroupComponentDescriptors-custom.h +22 -0
- package/shared/PCButtonGroupShadowNode-custom.cpp +48 -0
- package/shared/PCButtonGroupShadowNode-custom.h +55 -0
- package/shared/PCButtonGroupState-custom.h +61 -0
- package/shared/PCButtonShadowNode-custom.cpp +48 -0
- package/shared/PCButtonShadowNode-custom.h +55 -0
- package/shared/PCButtonState-custom.h +61 -0
- package/shared/PCTextFieldComponentDescriptors-custom.h +22 -0
- package/shared/PCTextFieldShadowNode-custom.cpp +47 -0
- package/shared/PCTextFieldShadowNode-custom.h +55 -0
- package/shared/PCTextFieldState-custom.h +61 -0
- package/shared/react/renderer/components/PlatformComponentsViewSpec/ComponentDescriptors.h +3 -0
- package/src/Button.tsx +144 -0
- package/src/ButtonGroup.tsx +233 -0
- package/src/ButtonGroupNativeComponent.ts +121 -0
- package/src/ButtonNativeComponent.ts +76 -0
- package/src/FloatingToolbar.tsx +113 -0
- package/src/FloatingToolbarNativeComponent.ts +31 -0
- package/src/SegmentedControl.tsx +26 -118
- package/src/SegmentedControlNativeComponent.ts +3 -0
- package/src/TextField.tsx +494 -0
- package/src/TextFieldNativeComponent.ts +218 -0
- package/src/focusRegistry.ts +89 -0
- package/src/icons.ts +116 -0
- package/src/index.tsx +6 -0
- package/src/index.web.tsx +194 -0
- package/src/labelStyle.ts +36 -0
- package/src/reactNativeInternals.d.ts +22 -0
- package/src/sharedTypes.ts +7 -1
package/README.md
CHANGED
|
@@ -5,34 +5,22 @@
|
|
|
5
5
|
[](https://github.com/JarX-Concepts/react-native-platform-components/actions/workflows/ci.yml)
|
|
6
6
|
[](./LICENSE)
|
|
7
7
|
|
|
8
|
-
Native **DatePicker**, **ContextMenu**, **SelectionMenu**, **SegmentedControl** and **LiquidGlass** for React Native. Every component is the real platform widget on both iOS and Android (UIKit and SwiftUI on iOS, Material 3 on Android), behind one typed, declarative API. No JavaScript re-implementations.
|
|
9
|
-
|
|
10
|
-
<
|
|
11
|
-
<
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
<td align="center"><a href="https://jarx-concepts.github.io/react-native-platform-components/components/liquidglass"><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/ios-liquidglass.gif" width="100" alt="LiquidGlass on iOS" /></a></td>
|
|
25
|
-
<td align="center"><a href="https://jarx-concepts.github.io/react-native-platform-components/guides/theming"><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/ios-theme.gif" width="100" alt="Brand color and dark mode on iOS" /></a></td>
|
|
26
|
-
</tr>
|
|
27
|
-
<tr>
|
|
28
|
-
<td align="center"><a href="https://jarx-concepts.github.io/react-native-platform-components/components/datepicker"><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/android-datepicker.gif" width="100" alt="DatePicker on Android" /></a></td>
|
|
29
|
-
<td align="center"><a href="https://jarx-concepts.github.io/react-native-platform-components/components/contextmenu"><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/android-contextmenu.gif" width="100" alt="ContextMenu on Android" /></a></td>
|
|
30
|
-
<td align="center"><a href="https://jarx-concepts.github.io/react-native-platform-components/components/selectionmenu"><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/android-selectionmenu.gif" width="100" alt="SelectionMenu on Android" /></a></td>
|
|
31
|
-
<td align="center"><a href="https://jarx-concepts.github.io/react-native-platform-components/components/segmentedcontrol"><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/android-segmentedcontrol.gif" width="100" alt="SegmentedControl on Android" /></a></td>
|
|
32
|
-
<td align="center" valign="middle"><sub>iOS 26+ only.<br/>Plain View on Android.</sub></td>
|
|
33
|
-
<td align="center"><a href="https://jarx-concepts.github.io/react-native-platform-components/guides/theming"><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/android-theme.gif" width="100" alt="Brand color and dark mode on Android" /></a></td>
|
|
34
|
-
</tr>
|
|
35
|
-
</table>
|
|
8
|
+
Native **TextField**, **DatePicker**, **ContextMenu**, **SelectionMenu**, **SegmentedControl**, **Button**, **ButtonGroup**, **FloatingToolbar** and **LiquidGlass** for React Native. Every component is the real platform widget on both iOS and Android (UIKit and SwiftUI on iOS, Material 3 Expressive on Android), behind one typed, declarative API. No JavaScript re-implementations.
|
|
9
|
+
|
|
10
|
+
<p align="center">
|
|
11
|
+
<a href="https://jarx-concepts.github.io/react-native-platform-components">
|
|
12
|
+
<picture>
|
|
13
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/hero-dark.webp" />
|
|
14
|
+
<img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/hero-light.webp" alt="DatePicker, TextField, SegmentedControl, Button, ContextMenu, LiquidGlass, FloatingToolbar and SelectionMenu on iOS and Android, and the dark mode from useNativeTheme" />
|
|
15
|
+
</picture>
|
|
16
|
+
</a>
|
|
17
|
+
</p>
|
|
18
|
+
<p align="center"><sub>Every tile is the platform's own widget, captured from the example app. The <a href="https://jarx-concepts.github.io/react-native-platform-components">docs site</a> has full recordings of each component on both platforms; <a href="https://jarx-concepts.github.io/react-native-platform-components/components/liquidglass">LiquidGlass</a> is iOS 26 only.</sub></p>
|
|
19
|
+
|
|
20
|
+
<p align="center">
|
|
21
|
+
<a href="https://jarx-concepts.github.io/react-native-platform-components"><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/showreel.gif" width="640" alt="Showreel: the native theme changing brand color and switching to dark mode, then DatePicker, TextField, SegmentedControl, Button, ContextMenu, SelectionMenu, FloatingToolbar and LiquidGlass in use, iOS on the left and Android on the right" /></a>
|
|
22
|
+
</p>
|
|
23
|
+
<p align="center"><sub>iOS on the left, Android on the right. Recorded by the Detox suite in <code>example/</code>.</sub></p>
|
|
36
24
|
|
|
37
25
|
```sh
|
|
38
26
|
npm install react-native-platform-components
|
|
@@ -57,10 +45,12 @@ That renders `UISegmentedControl` on iOS and Material 3 segmented buttons (`Mate
|
|
|
57
45
|
|
|
58
46
|
| You need | This library | Common alternative |
|
|
59
47
|
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
48
|
+
| Text field | Material 3 `TextInputLayout` on Android (floating label, outlined or filled, supporting text, error state, icons, counter); `UITextField` on iOS with Writing Tools, inline predictions and smart punctuation. Same `value` / `onChangeText` contract as `TextInput`. | `TextInput`: a bare `EditText` on Android, no Material field; the iOS 17 and 18 text traits are not exposed. `react-native-paper`: Material 3 drawn in JavaScript. |
|
|
60
49
|
| Date and time pickers | `UIDatePicker` on iOS; `MaterialDatePicker` / `MaterialTimePicker` or the AppCompat dialogs on Android. Declarative `visible` prop, modal or embedded. | `@react-native-community/datetimepicker`: platform dialogs on Android; Material 3 pickers are an [open request](https://github.com/react-native-datetimepicker/datetimepicker/issues/790). |
|
|
61
50
|
| Segmented control | `UISegmentedControl` on iOS; Material 3 segmented buttons on Android. Icons and badges on both. | `@react-native-segmented-control/segmented-control`: native on iOS, drawn in JavaScript on Android. |
|
|
62
51
|
| Context menu | `UIContextMenuInteraction` on iOS; `PopupMenu` on Android. Long-press gesture or modal trigger, icons on both. | `zeego`: native on both platforms, through two additional native dependencies (`react-native-ios-context-menu`, `@react-native-menu/menu`). |
|
|
63
52
|
| Selection menu | System menus on iOS; Material exposed dropdown or `Spinner` on Android. Headless or inline. | `@react-native-picker/picker`: wheel picker on iOS, dialog or dropdown `Spinner` on Android; no menu-style presentation. |
|
|
53
|
+
| Buttons and toolbars | Material 3 Expressive `MaterialButton`, button groups and `FloatingToolbarLayout` on Android (five sizes, shape morphing); `UIButton` configurations and a Liquid Glass toolbar on iOS. | `react-native-paper`: Material 3 (not Expressive) drawn in JavaScript on both platforms. |
|
|
64
54
|
| Liquid glass | `UIGlassEffect` on iOS 26+, a fallback `View` elsewhere, `isLiquidGlassSupported` flag. | `@callstack/liquid-glass`, `expo-glass-effect`: same idea on iOS; here it ships with the components above. |
|
|
65
55
|
| All of the above | One package, one API shape (platform-only props under `ios={{ }}` and `android={{ }}`), Fabric + Codegen typed bindings, Expo config plugin, TypeScript. | Four or five packages with different conventions, install steps and upgrade cadences. |
|
|
66
56
|
|
|
@@ -68,10 +58,14 @@ That renders `UISegmentedControl` on iOS and Material 3 segmented buttons (`Mate
|
|
|
68
58
|
|
|
69
59
|
| Component | iOS | Android | Docs |
|
|
70
60
|
| -------------------- | -------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
61
|
+
| **TextField** | `UITextField` / `UITextView` | `TextInputLayout` (Material 3: outlined or filled, label, supporting text, icons, counter) | [TextField](https://jarx-concepts.github.io/react-native-platform-components/components/textfield) |
|
|
71
62
|
| **DatePicker** | `UIDatePicker` | `MaterialDatePicker` / `MaterialTimePicker` or AppCompat dialogs | [DatePicker](https://jarx-concepts.github.io/react-native-platform-components/components/datepicker) |
|
|
72
63
|
| **ContextMenu** | `UIContextMenuInteraction` | `PopupMenu` | [ContextMenu](https://jarx-concepts.github.io/react-native-platform-components/components/contextmenu) |
|
|
73
64
|
| **SelectionMenu** | System menus | Material exposed dropdown or `Spinner` | [SelectionMenu](https://jarx-concepts.github.io/react-native-platform-components/components/selectionmenu) |
|
|
74
|
-
| **SegmentedControl** | `UISegmentedControl` | `MaterialButtonToggleGroup` (Material 3
|
|
65
|
+
| **SegmentedControl** | `UISegmentedControl` | `MaterialButtonToggleGroup` (Material 3 Expressive connected buttons, or classic) | [SegmentedControl](https://jarx-concepts.github.io/react-native-platform-components/components/segmentedcontrol) |
|
|
66
|
+
| **Button** | `UIButton` (configurations) | `MaterialButton` (Material 3 Expressive: five sizes, round or square, shape morph) | [Button](https://jarx-concepts.github.io/react-native-platform-components/components/button) |
|
|
67
|
+
| **ButtonGroup** | Row of `UIButton`s | `MaterialButtonGroup` / `MaterialButtonToggleGroup` (Material 3 Expressive) | [ButtonGroup](https://jarx-concepts.github.io/react-native-platform-components/components/buttongroup) |
|
|
68
|
+
| **FloatingToolbar** | `UIGlassEffect` capsule (iOS 26+), blur below | `FloatingToolbarLayout` (Material 3 Expressive) | [FloatingToolbar](https://jarx-concepts.github.io/react-native-platform-components/components/floatingtoolbar) |
|
|
75
69
|
| **LiquidGlass** | `UIGlassEffect` (iOS 26+) | Fallback `View` | [LiquidGlass](https://jarx-concepts.github.io/react-native-platform-components/components/liquidglass) |
|
|
76
70
|
|
|
77
71
|
**Have a component request?** If there's a native UI component you'd like to see added, [open an issue](https://github.com/JarX-Concepts/react-native-platform-components/issues/new) describing the component and its native APIs on iOS and Android.
|
|
@@ -140,7 +134,7 @@ Full details: [Installation](https://jarx-concepts.github.io/react-native-platfo
|
|
|
140
134
|
Everything else lives on the documentation site:
|
|
141
135
|
|
|
142
136
|
- [Quick Start](https://jarx-concepts.github.io/react-native-platform-components/quick-start): copy-paste examples for every component
|
|
143
|
-
- [DatePicker](https://jarx-concepts.github.io/react-native-platform-components/components/datepicker), [ContextMenu](https://jarx-concepts.github.io/react-native-platform-components/components/contextmenu), [SelectionMenu](https://jarx-concepts.github.io/react-native-platform-components/components/selectionmenu), [SegmentedControl](https://jarx-concepts.github.io/react-native-platform-components/components/segmentedcontrol), [LiquidGlass](https://jarx-concepts.github.io/react-native-platform-components/components/liquidglass): props, platform options and behavior
|
|
137
|
+
- [DatePicker](https://jarx-concepts.github.io/react-native-platform-components/components/datepicker), [ContextMenu](https://jarx-concepts.github.io/react-native-platform-components/components/contextmenu), [SelectionMenu](https://jarx-concepts.github.io/react-native-platform-components/components/selectionmenu), [SegmentedControl](https://jarx-concepts.github.io/react-native-platform-components/components/segmentedcontrol), [Button](https://jarx-concepts.github.io/react-native-platform-components/components/button), [ButtonGroup](https://jarx-concepts.github.io/react-native-platform-components/components/buttongroup), [FloatingToolbar](https://jarx-concepts.github.io/react-native-platform-components/components/floatingtoolbar), [LiquidGlass](https://jarx-concepts.github.io/react-native-platform-components/components/liquidglass): props, platform options and behavior
|
|
144
138
|
- [Android Theme Configuration](https://jarx-concepts.github.io/react-native-platform-components/guides/android-theme): Material 3 setup and what happens without it
|
|
145
139
|
- [Icons](https://jarx-concepts.github.io/react-native-platform-components/guides/icons): SF Symbols, drawables and images
|
|
146
140
|
- [Theming and Colors](https://jarx-concepts.github.io/react-native-platform-components/guides/theming): light/dark mode and your brand color from JavaScript (`useNativeTheme`), accepted color formats
|
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
package com.platformcomponents
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.text.TextUtils
|
|
5
|
+
import android.view.ContextThemeWrapper
|
|
6
|
+
import android.view.View
|
|
7
|
+
import android.view.ViewGroup.MarginLayoutParams
|
|
8
|
+
import android.widget.FrameLayout
|
|
9
|
+
import com.facebook.react.bridge.WritableNativeMap
|
|
10
|
+
import com.facebook.react.uimanager.PixelUtil
|
|
11
|
+
import com.facebook.react.uimanager.ReactCompoundViewGroup
|
|
12
|
+
import com.facebook.react.uimanager.StateWrapper
|
|
13
|
+
import com.facebook.react.views.scroll.ReactScrollViewHelper
|
|
14
|
+
import com.google.android.material.button.MaterialButton
|
|
15
|
+
import com.google.android.material.button.MaterialButtonGroup
|
|
16
|
+
import com.google.android.material.button.MaterialButtonToggleGroup
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A Material 3 Expressive button group: MaterialButtonGroup for plain actions,
|
|
20
|
+
* MaterialButtonToggleGroup for single / multiple selection. Rebuilt whenever
|
|
21
|
+
* a prop the widgets read at construction changes.
|
|
22
|
+
*/
|
|
23
|
+
class PCButtonGroupView(context: Context) :
|
|
24
|
+
FrameLayout(context),
|
|
25
|
+
ReactScrollViewHelper.HasStateWrapper,
|
|
26
|
+
ReactCompoundViewGroup {
|
|
27
|
+
|
|
28
|
+
data class Item(
|
|
29
|
+
val label: String,
|
|
30
|
+
val value: String,
|
|
31
|
+
val disabled: Boolean,
|
|
32
|
+
val icon: PCButtonSupport.Icon,
|
|
33
|
+
val accessibilityLabel: String
|
|
34
|
+
) {
|
|
35
|
+
/** What TalkBack announces for the button. */
|
|
36
|
+
val spokenLabel: String get() = accessibilityLabel.ifEmpty { label }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
companion object {
|
|
40
|
+
private const val TAG = "PCButtonGroup"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// --- State Wrapper for Fabric state updates ---
|
|
44
|
+
override var stateWrapper: StateWrapper? = null
|
|
45
|
+
|
|
46
|
+
private var lastReportedWidth: Float = 0f
|
|
47
|
+
private var lastReportedHeight: Float = 0f
|
|
48
|
+
|
|
49
|
+
// --- Props ---
|
|
50
|
+
var items: List<Item> = emptyList()
|
|
51
|
+
var variant: PCExpressive.Variant = PCExpressive.Variant.OUTLINED
|
|
52
|
+
var size: String = "small"
|
|
53
|
+
var shape: String = "round"
|
|
54
|
+
var connected: Boolean = false
|
|
55
|
+
var spacing: Float = -1f // dp; negative = style default
|
|
56
|
+
var selection: String = "none" // "none" | "single" | "multiple"
|
|
57
|
+
var selectedValues: List<String> = emptyList()
|
|
58
|
+
var selectionRequired: Boolean = false
|
|
59
|
+
var interactivity: String = "enabled" // "enabled" | "disabled"
|
|
60
|
+
var overflow: String = "none" // "none" | "menu" | "wrap"
|
|
61
|
+
var expressive: Boolean = true // android.material: "expressive" | "m3"
|
|
62
|
+
|
|
63
|
+
// --- Styling (null / empty = Material theme default) ---
|
|
64
|
+
var containerColor: Int? = null
|
|
65
|
+
var foregroundColor: Int? = null
|
|
66
|
+
var rippleColor: Int? = null
|
|
67
|
+
var strokeColor: Int? = null
|
|
68
|
+
var labelFontFamily: String = ""
|
|
69
|
+
var labelFontSize: Float = 0f
|
|
70
|
+
var labelFontWeight: String = ""
|
|
71
|
+
var labelFontStyle: String = ""
|
|
72
|
+
|
|
73
|
+
// --- Events ---
|
|
74
|
+
var onPress: ((index: Int, value: String) -> Unit)? = null
|
|
75
|
+
var onSelectionChange: ((values: List<String>) -> Unit)? = null
|
|
76
|
+
|
|
77
|
+
// --- UI ---
|
|
78
|
+
private var group: MaterialButtonGroup? = null
|
|
79
|
+
private val buttonIdToIndex: MutableMap<Int, Int> = mutableMapOf()
|
|
80
|
+
private var suppressCallbacks = false
|
|
81
|
+
private var selectionEmitPending = false
|
|
82
|
+
|
|
83
|
+
/** Bumped on every rebuild so late image loads can't touch stale buttons. */
|
|
84
|
+
private var rebuildGeneration = 0
|
|
85
|
+
|
|
86
|
+
// --- Native theme ---
|
|
87
|
+
private var builtThemeVersion = PCNativeTheme.version
|
|
88
|
+
private val nativeThemeListener = PCNativeTheme.Listener {
|
|
89
|
+
PCThemeSupport.clearColorStateListCaches(this)
|
|
90
|
+
rebuildUI()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
init {
|
|
94
|
+
clipChildren = false
|
|
95
|
+
clipToPadding = false
|
|
96
|
+
rebuildUI()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---- Public apply* (called by manager) ----
|
|
100
|
+
|
|
101
|
+
fun applyItems(value: List<Item>) {
|
|
102
|
+
if (items == value) return
|
|
103
|
+
items = value
|
|
104
|
+
rebuildUI()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
fun applyVariant(value: String?) {
|
|
108
|
+
val parsed = PCExpressive.parseVariant(value)
|
|
109
|
+
if (variant == parsed) return
|
|
110
|
+
variant = parsed
|
|
111
|
+
rebuildUI()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fun applySize(value: String?) {
|
|
115
|
+
val parsed = PCExpressive.parseSize(value)
|
|
116
|
+
if (size == parsed) return
|
|
117
|
+
size = parsed
|
|
118
|
+
rebuildUI()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
fun applyShape(value: String?) {
|
|
122
|
+
val parsed = PCExpressive.parseShape(value)
|
|
123
|
+
if (shape == parsed) return
|
|
124
|
+
shape = parsed
|
|
125
|
+
rebuildUI()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
fun applyConnected(value: Boolean) {
|
|
129
|
+
if (connected == value) return
|
|
130
|
+
connected = value
|
|
131
|
+
rebuildUI()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
fun applySpacing(value: Float) {
|
|
135
|
+
if (spacing == value) return
|
|
136
|
+
spacing = value
|
|
137
|
+
rebuildUI()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
fun applySelection(value: String?) {
|
|
141
|
+
val parsed = when (value) {
|
|
142
|
+
"single", "multiple" -> value
|
|
143
|
+
else -> "none"
|
|
144
|
+
}
|
|
145
|
+
if (selection == parsed) return
|
|
146
|
+
selection = parsed
|
|
147
|
+
rebuildUI()
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
fun applySelectedValues(value: List<String>) {
|
|
151
|
+
if (selectedValues == value) return
|
|
152
|
+
selectedValues = value
|
|
153
|
+
updateSelection()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
fun applySelectionRequired(value: Boolean) {
|
|
157
|
+
if (selectionRequired == value) return
|
|
158
|
+
selectionRequired = value
|
|
159
|
+
(group as? MaterialButtonToggleGroup)?.isSelectionRequired = value
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
fun applyInteractivity(value: String?) {
|
|
163
|
+
val newValue = if (value == "disabled") "disabled" else "enabled"
|
|
164
|
+
if (interactivity == newValue) return
|
|
165
|
+
interactivity = newValue
|
|
166
|
+
updateEnabled()
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
fun applyOverflow(value: String?) {
|
|
170
|
+
val parsed = when (value) {
|
|
171
|
+
"menu", "wrap" -> value
|
|
172
|
+
else -> "none"
|
|
173
|
+
}
|
|
174
|
+
if (overflow == parsed) return
|
|
175
|
+
overflow = parsed
|
|
176
|
+
rebuildUI()
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
fun applyMaterial(value: String?) {
|
|
180
|
+
val parsed = PCExpressive.parseExpressive(value)
|
|
181
|
+
if (expressive == parsed) return
|
|
182
|
+
expressive = parsed
|
|
183
|
+
rebuildUI()
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
fun applyColors(container: Int?, foreground: Int?, ripple: Int?, stroke: Int?) {
|
|
187
|
+
if (containerColor == container && foregroundColor == foreground &&
|
|
188
|
+
rippleColor == ripple && strokeColor == stroke
|
|
189
|
+
) return
|
|
190
|
+
containerColor = container
|
|
191
|
+
foregroundColor = foreground
|
|
192
|
+
rippleColor = ripple
|
|
193
|
+
strokeColor = stroke
|
|
194
|
+
rebuildUI()
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
fun applyLabelStyle(fontFamily: String, fontSize: Float, fontWeight: String, fontStyle: String) {
|
|
198
|
+
if (labelFontFamily == fontFamily && labelFontSize == fontSize &&
|
|
199
|
+
labelFontWeight == fontWeight && labelFontStyle == fontStyle
|
|
200
|
+
) return
|
|
201
|
+
labelFontFamily = fontFamily
|
|
202
|
+
labelFontSize = fontSize
|
|
203
|
+
labelFontWeight = fontWeight
|
|
204
|
+
labelFontStyle = fontStyle
|
|
205
|
+
rebuildUI()
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// The buttons have generated view ids, which React Native's touch handling would
|
|
209
|
+
// take for React tags and dispatch JS touch events to unrelated views. Claim the
|
|
210
|
+
// touch for this view instead; the buttons still receive it natively.
|
|
211
|
+
override fun interceptsTouchEvent(touchX: Float, touchY: Float): Boolean = true
|
|
212
|
+
|
|
213
|
+
override fun reactTagForTouch(touchX: Float, touchY: Float): Int = id
|
|
214
|
+
|
|
215
|
+
override fun onAttachedToWindow() {
|
|
216
|
+
super.onAttachedToWindow()
|
|
217
|
+
PCNativeTheme.addListener(nativeThemeListener)
|
|
218
|
+
PCNativeTheme.attach(context)
|
|
219
|
+
if (builtThemeVersion != PCNativeTheme.version) nativeThemeListener.onNativeThemeChanged()
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
override fun onDetachedFromWindow() {
|
|
223
|
+
PCNativeTheme.removeListener(nativeThemeListener)
|
|
224
|
+
super.onDetachedFromWindow()
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ---- UI Building ----
|
|
228
|
+
|
|
229
|
+
private fun rebuildUI() {
|
|
230
|
+
builtThemeVersion = PCNativeTheme.version
|
|
231
|
+
removeAllViews()
|
|
232
|
+
buttonIdToIndex.clear()
|
|
233
|
+
rebuildGeneration += 1
|
|
234
|
+
val generation = rebuildGeneration
|
|
235
|
+
|
|
236
|
+
// Material widgets need a Material theme; fall back to Material 3 defaults instead of crashing.
|
|
237
|
+
val base = PCThemeSupport.materialContext(context, "ButtonGroup")
|
|
238
|
+
val toggle = selection != "none"
|
|
239
|
+
|
|
240
|
+
// The group styles: a standard group (spacing, press size morph) or a
|
|
241
|
+
// connected one (shared outline, small inner corners), Expressive or
|
|
242
|
+
// classic Material 3.
|
|
243
|
+
val overlay = when {
|
|
244
|
+
expressive && toggle && !connected -> R.style.PCExpressiveOverlay_StandardToggleGroup
|
|
245
|
+
expressive && !toggle && connected -> R.style.PCExpressiveOverlay_ConnectedGroup
|
|
246
|
+
expressive -> R.style.PCExpressiveOverlay
|
|
247
|
+
toggle && !connected -> R.style.PCM3Overlay_StandardToggleGroup
|
|
248
|
+
!toggle && connected -> R.style.PCM3Overlay_ConnectedGroup
|
|
249
|
+
else -> 0
|
|
250
|
+
}
|
|
251
|
+
val groupContext = if (overlay != 0) ContextThemeWrapper(base, overlay) else base
|
|
252
|
+
|
|
253
|
+
val g: MaterialButtonGroup =
|
|
254
|
+
if (toggle) {
|
|
255
|
+
MaterialButtonToggleGroup(groupContext).apply {
|
|
256
|
+
isSingleSelection = selection == "single"
|
|
257
|
+
isSelectionRequired = selectionRequired
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
MaterialButtonGroup(groupContext)
|
|
261
|
+
}
|
|
262
|
+
g.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
|
|
263
|
+
g.clipChildren = false
|
|
264
|
+
g.clipToPadding = false
|
|
265
|
+
if (spacing >= 0) {
|
|
266
|
+
g.spacing = (spacing * resources.displayMetrics.density).toInt()
|
|
267
|
+
}
|
|
268
|
+
g.overflowMode = when (overflow) {
|
|
269
|
+
"menu" -> MaterialButtonGroup.OVERFLOW_MODE_MENU
|
|
270
|
+
"wrap" -> MaterialButtonGroup.OVERFLOW_MODE_WRAP
|
|
271
|
+
else -> MaterialButtonGroup.OVERFLOW_MODE_NONE
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
for ((index, item) in items.withIndex()) {
|
|
275
|
+
val iconOnly = item.label.isEmpty() && item.icon.isPresent
|
|
276
|
+
|
|
277
|
+
val button = PCExpressive.createButton(
|
|
278
|
+
base, variant, size, shape, iconOnly, 0, expressive
|
|
279
|
+
).apply {
|
|
280
|
+
id = View.generateViewId()
|
|
281
|
+
text = item.label
|
|
282
|
+
isAllCaps = false // Preserve original text casing
|
|
283
|
+
ellipsize = TextUtils.TruncateAt.END
|
|
284
|
+
maxLines = 1
|
|
285
|
+
contentDescription = item.spokenLabel.ifEmpty { null }
|
|
286
|
+
isEnabled = !item.disabled && interactivity == "enabled"
|
|
287
|
+
if (iconOnly) {
|
|
288
|
+
// Centre the icon instead of leaving it at the start edge
|
|
289
|
+
iconGravity = MaterialButton.ICON_GRAVITY_TEXT_START
|
|
290
|
+
}
|
|
291
|
+
setOnClickListener { onPress?.invoke(index, item.value) }
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
PCButtonSupport.applyIcon(button, item.icon, { generation == rebuildGeneration }) { requestLayout() }
|
|
295
|
+
PCButtonSupport.applyFont(button, labelFontFamily, labelFontSize, labelFontWeight, labelFontStyle)
|
|
296
|
+
PCButtonSupport.applyColors(button, containerColor, foregroundColor, rippleColor, strokeColor, item.icon.tinted)
|
|
297
|
+
|
|
298
|
+
buttonIdToIndex[button.id] = index
|
|
299
|
+
// Buttons size to their content. Connected buttons also share any extra
|
|
300
|
+
// width when the group is stretched (and any shortfall when it is
|
|
301
|
+
// squeezed) equally; standard buttons keep their own width.
|
|
302
|
+
val params = MaterialButtonGroup.LayoutParams(
|
|
303
|
+
LayoutParams.WRAP_CONTENT,
|
|
304
|
+
LayoutParams.WRAP_CONTENT,
|
|
305
|
+
if (connected) 1f else 0f
|
|
306
|
+
)
|
|
307
|
+
params.overflowText = item.spokenLabel
|
|
308
|
+
g.addView(button, params)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (g is MaterialButtonToggleGroup) {
|
|
312
|
+
g.addOnButtonCheckedListener { _, _, _ ->
|
|
313
|
+
if (suppressCallbacks) return@addOnButtonCheckedListener
|
|
314
|
+
// A single-selection switch reports the old button unchecked and the
|
|
315
|
+
// new one checked in turn; report the final selection once.
|
|
316
|
+
scheduleSelectionEmit()
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
addView(g)
|
|
321
|
+
group = g
|
|
322
|
+
|
|
323
|
+
updateSelection()
|
|
324
|
+
updateEnabled()
|
|
325
|
+
requestLayout()
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private fun scheduleSelectionEmit() {
|
|
329
|
+
if (selectionEmitPending) return
|
|
330
|
+
selectionEmitPending = true
|
|
331
|
+
post {
|
|
332
|
+
selectionEmitPending = false
|
|
333
|
+
emitSelection()
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
private fun emitSelection() {
|
|
338
|
+
val g = group as? MaterialButtonToggleGroup ?: return
|
|
339
|
+
val values = g.checkedButtonIds
|
|
340
|
+
.mapNotNull { buttonIdToIndex[it] }
|
|
341
|
+
.sorted()
|
|
342
|
+
.map { items[it].value }
|
|
343
|
+
onSelectionChange?.invoke(values)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private fun updateSelection() {
|
|
347
|
+
val g = group as? MaterialButtonToggleGroup ?: return
|
|
348
|
+
suppressCallbacks = true
|
|
349
|
+
for ((id, index) in buttonIdToIndex) {
|
|
350
|
+
val checked = selectedValues.contains(items[index].value)
|
|
351
|
+
if (checked) g.check(id) else g.uncheck(id)
|
|
352
|
+
}
|
|
353
|
+
suppressCallbacks = false
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private fun updateEnabled() {
|
|
357
|
+
val enabled = interactivity == "enabled"
|
|
358
|
+
alpha = if (enabled) 1f else 0.5f
|
|
359
|
+
|
|
360
|
+
val g = group ?: return
|
|
361
|
+
for (i in 0 until g.childCount) {
|
|
362
|
+
val button = g.getChildAt(i) as? MaterialButton ?: continue
|
|
363
|
+
val index = buttonIdToIndex[button.id] ?: continue
|
|
364
|
+
button.isEnabled = enabled && !items[index].disabled
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ---- Layout ----
|
|
369
|
+
|
|
370
|
+
private var manualLayoutPending = false
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* React Native's root view ignores requestLayout after the initial pass, so
|
|
374
|
+
* a native view that changes its own subtree later has to measure and lay
|
|
375
|
+
* itself out. Coalesced to one pass per frame.
|
|
376
|
+
*/
|
|
377
|
+
override fun requestLayout() {
|
|
378
|
+
super.requestLayout()
|
|
379
|
+
if (!manualLayoutPending) {
|
|
380
|
+
manualLayoutPending = true
|
|
381
|
+
post { runManualLayout() }
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private fun runManualLayout() {
|
|
386
|
+
manualLayoutPending = false
|
|
387
|
+
// Nothing to do until Yoga has placed us; the regular pass covers that.
|
|
388
|
+
if (width == 0 || height == 0) return
|
|
389
|
+
measure(
|
|
390
|
+
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
|
391
|
+
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
|
|
392
|
+
)
|
|
393
|
+
layout(left, top, right, bottom)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ---- Measurement ----
|
|
397
|
+
|
|
398
|
+
private val unspecified = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* The group's natural width: every button on one row. MaterialButtonGroup
|
|
402
|
+
* answers that itself, except in the "wrap" overflow mode, which refuses an
|
|
403
|
+
* AT_MOST width and lays every button on its own row for UNSPECIFIED; there
|
|
404
|
+
* the buttons are summed instead.
|
|
405
|
+
*/
|
|
406
|
+
private fun naturalWidth(child: MaterialButtonGroup): Int {
|
|
407
|
+
if (overflow != "wrap") {
|
|
408
|
+
child.measure(unspecified, unspecified)
|
|
409
|
+
return child.measuredWidth
|
|
410
|
+
}
|
|
411
|
+
var total = child.paddingLeft + child.paddingRight
|
|
412
|
+
var visible = 0
|
|
413
|
+
for (i in 0 until child.childCount) {
|
|
414
|
+
val button = child.getChildAt(i)
|
|
415
|
+
if (button.visibility == View.GONE) continue
|
|
416
|
+
button.measure(unspecified, unspecified)
|
|
417
|
+
val params = button.layoutParams as? MarginLayoutParams
|
|
418
|
+
total += button.measuredWidth + (params?.leftMargin ?: 0) + (params?.rightMargin ?: 0)
|
|
419
|
+
visible++
|
|
420
|
+
}
|
|
421
|
+
if (visible > 1) total += child.spacing * (visible - 1)
|
|
422
|
+
return total
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
426
|
+
val child = group
|
|
427
|
+
if (child == null) {
|
|
428
|
+
setMeasuredDimension(0, 0)
|
|
429
|
+
return
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
val natural = naturalWidth(child)
|
|
433
|
+
val width = resolveDimension(widthMeasureSpec, natural)
|
|
434
|
+
child.measure(MeasureSpec.makeMeasureSpec(minOf(width, natural), MeasureSpec.EXACTLY), unspecified)
|
|
435
|
+
val height = resolveDimension(heightMeasureSpec, child.measuredHeight)
|
|
436
|
+
|
|
437
|
+
child.measure(
|
|
438
|
+
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
|
439
|
+
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
|
|
440
|
+
)
|
|
441
|
+
setMeasuredDimension(width, height)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
private fun resolveDimension(spec: Int, desired: Int): Int {
|
|
445
|
+
val size = MeasureSpec.getSize(spec)
|
|
446
|
+
return when (MeasureSpec.getMode(spec)) {
|
|
447
|
+
// Fabric may give us 0 before the state is applied; keep the natural size then.
|
|
448
|
+
MeasureSpec.EXACTLY -> if (size > 0) size else desired
|
|
449
|
+
MeasureSpec.AT_MOST -> minOf(desired, size)
|
|
450
|
+
else -> desired
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
455
|
+
super.onLayout(changed, left, top, right, bottom)
|
|
456
|
+
updateFrameSizeState()
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** Reports the intrinsic size to Fabric; called as soon as the state wrapper is available. */
|
|
460
|
+
fun reportIntrinsicSize() = updateFrameSizeState()
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Update Fabric state with the group's natural size, so the shadow node
|
|
464
|
+
* wraps its content in Yoga layout.
|
|
465
|
+
*/
|
|
466
|
+
private fun updateFrameSizeState() {
|
|
467
|
+
val wrapper = stateWrapper ?: return
|
|
468
|
+
val child = group ?: return
|
|
469
|
+
|
|
470
|
+
// The natural width, and the height at the width we actually have when
|
|
471
|
+
// that is narrower (an overflow "wrap" group grows taller then).
|
|
472
|
+
val naturalWidth = naturalWidth(child)
|
|
473
|
+
val fittedWidth = if (width in 1 until naturalWidth) width else naturalWidth
|
|
474
|
+
child.measure(MeasureSpec.makeMeasureSpec(fittedWidth, MeasureSpec.EXACTLY), unspecified)
|
|
475
|
+
val widthDp = PixelUtil.toDIPFromPixel(naturalWidth.toFloat())
|
|
476
|
+
val heightDp = PixelUtil.toDIPFromPixel(child.measuredHeight.toFloat())
|
|
477
|
+
|
|
478
|
+
// Measuring re-wraps the labels; restore the laid-out size so they are
|
|
479
|
+
// ellipsized to the bounds they are drawn in.
|
|
480
|
+
if (child.width > 0 && child.height > 0) {
|
|
481
|
+
child.measure(
|
|
482
|
+
MeasureSpec.makeMeasureSpec(child.width, MeasureSpec.EXACTLY),
|
|
483
|
+
MeasureSpec.makeMeasureSpec(child.height, MeasureSpec.EXACTLY)
|
|
484
|
+
)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (widthDp != lastReportedWidth || heightDp != lastReportedHeight) {
|
|
488
|
+
lastReportedWidth = widthDp
|
|
489
|
+
lastReportedHeight = heightDp
|
|
490
|
+
|
|
491
|
+
val stateData = WritableNativeMap().apply {
|
|
492
|
+
putDouble("width", widthDp.toDouble())
|
|
493
|
+
putDouble("height", heightDp.toDouble())
|
|
494
|
+
}
|
|
495
|
+
wrapper.updateState(stateData)
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|