react-native-platform-components 1.2.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 +33 -31
- 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/PCNativeThemeModule.kt +3 -0
- package/android/src/main/java/com/platformcomponents/PCSegmentedControlView.kt +13 -5
- 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 +13 -6
- package/android/src/main/jni/CMakeLists.txt +13 -1
- 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/PCNativeThemeModule.mm +13 -0
- package/ios/PCSelectionMenu.swift +9 -1
- 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/ContextMenuNativeComponent.ts +5 -4
- package/lib/commonjs/DatePickerNativeComponent.ts +17 -11
- 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/LiquidGlass.js +24 -4
- package/lib/commonjs/LiquidGlass.js.map +1 -1
- package/lib/commonjs/LiquidGlassNativeComponent.ts +6 -5
- package/lib/commonjs/NativePlatformComponentsTheme.js.map +1 -1
- package/lib/commonjs/SegmentedControl.js +11 -85
- package/lib/commonjs/SegmentedControl.js.map +1 -1
- package/lib/commonjs/SegmentedControlNativeComponent.ts +15 -11
- package/lib/commonjs/SelectionMenuNativeComponent.ts +6 -5
- 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/codegenTypes.js +6 -0
- package/lib/commonjs/codegenTypes.js.map +1 -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/ContextMenuNativeComponent.ts +5 -4
- package/lib/module/DatePickerNativeComponent.ts +17 -11
- 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/LiquidGlass.js +24 -4
- package/lib/module/LiquidGlass.js.map +1 -1
- package/lib/module/LiquidGlassNativeComponent.ts +6 -5
- package/lib/module/NativePlatformComponentsTheme.js.map +1 -1
- package/lib/module/SegmentedControl.js +12 -87
- package/lib/module/SegmentedControl.js.map +1 -1
- package/lib/module/SegmentedControlNativeComponent.ts +15 -11
- package/lib/module/SelectionMenuNativeComponent.ts +6 -5
- 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/codegenTypes.js +4 -0
- package/lib/module/codegenTypes.js.map +1 -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/ContextMenuNativeComponent.d.ts +5 -4
- package/lib/typescript/commonjs/src/ContextMenuNativeComponent.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/DatePickerNativeComponent.d.ts +12 -11
- package/lib/typescript/commonjs/src/DatePickerNativeComponent.d.ts.map +1 -1
- 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/LiquidGlass.d.ts +4 -3
- package/lib/typescript/commonjs/src/LiquidGlass.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/LiquidGlassNativeComponent.d.ts +6 -5
- package/lib/typescript/commonjs/src/LiquidGlassNativeComponent.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/NativePlatformComponentsTheme.d.ts +10 -2
- package/lib/typescript/commonjs/src/NativePlatformComponentsTheme.d.ts.map +1 -1
- 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 +9 -6
- package/lib/typescript/commonjs/src/SegmentedControlNativeComponent.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/SelectionMenuNativeComponent.d.ts +6 -5
- package/lib/typescript/commonjs/src/SelectionMenuNativeComponent.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/codegenTypes.d.ts +12 -0
- package/lib/typescript/commonjs/src/codegenTypes.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 +8 -3
- 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/ContextMenuNativeComponent.d.ts +5 -4
- package/lib/typescript/module/src/ContextMenuNativeComponent.d.ts.map +1 -1
- package/lib/typescript/module/src/DatePickerNativeComponent.d.ts +12 -11
- package/lib/typescript/module/src/DatePickerNativeComponent.d.ts.map +1 -1
- 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/LiquidGlass.d.ts +4 -3
- package/lib/typescript/module/src/LiquidGlass.d.ts.map +1 -1
- package/lib/typescript/module/src/LiquidGlassNativeComponent.d.ts +6 -5
- package/lib/typescript/module/src/LiquidGlassNativeComponent.d.ts.map +1 -1
- package/lib/typescript/module/src/NativePlatformComponentsTheme.d.ts +10 -2
- package/lib/typescript/module/src/NativePlatformComponentsTheme.d.ts.map +1 -1
- 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 +9 -6
- package/lib/typescript/module/src/SegmentedControlNativeComponent.d.ts.map +1 -1
- package/lib/typescript/module/src/SelectionMenuNativeComponent.d.ts +6 -5
- package/lib/typescript/module/src/SelectionMenuNativeComponent.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/codegenTypes.d.ts +12 -0
- package/lib/typescript/module/src/codegenTypes.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 +8 -3
- package/lib/typescript/module/src/sharedTypes.d.ts.map +1 -1
- package/package.json +28 -6
- 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/PCDatePickerState-custom.h +2 -2
- package/shared/PCSegmentedControlState-custom.h +2 -2
- package/shared/PCSelectionMenuState-custom.h +2 -2
- 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/ContextMenuNativeComponent.ts +5 -4
- package/src/DatePickerNativeComponent.ts +17 -11
- package/src/FloatingToolbar.tsx +113 -0
- package/src/FloatingToolbarNativeComponent.ts +31 -0
- package/src/LiquidGlass.tsx +24 -5
- package/src/LiquidGlassNativeComponent.ts +6 -5
- package/src/NativePlatformComponentsTheme.ts +11 -2
- package/src/SegmentedControl.tsx +26 -118
- package/src/SegmentedControlNativeComponent.ts +15 -11
- package/src/SelectionMenuNativeComponent.ts +6 -5
- package/src/TextField.tsx +494 -0
- package/src/TextFieldNativeComponent.ts +218 -0
- package/src/codegenTypes.ts +40 -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 +9 -3
- /package/{app.plugin.cjs → app.plugin.js} +0 -0
|
@@ -0,0 +1,1018 @@
|
|
|
1
|
+
package com.platformcomponents
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Rect
|
|
5
|
+
import android.graphics.drawable.BitmapDrawable
|
|
6
|
+
import android.graphics.drawable.Drawable
|
|
7
|
+
import android.os.Build
|
|
8
|
+
import android.text.Editable
|
|
9
|
+
import android.text.InputFilter
|
|
10
|
+
import android.text.InputType
|
|
11
|
+
import android.text.TextWatcher
|
|
12
|
+
import android.view.ContextThemeWrapper
|
|
13
|
+
import android.view.Gravity
|
|
14
|
+
import android.view.KeyEvent
|
|
15
|
+
import android.view.View
|
|
16
|
+
import android.view.ViewGroup
|
|
17
|
+
import android.view.inputmethod.EditorInfo
|
|
18
|
+
import android.view.inputmethod.InputMethodManager
|
|
19
|
+
import android.widget.FrameLayout
|
|
20
|
+
import android.widget.LinearLayout
|
|
21
|
+
import android.widget.TextView
|
|
22
|
+
import android.content.res.ColorStateList
|
|
23
|
+
import android.util.TypedValue
|
|
24
|
+
import android.view.MotionEvent
|
|
25
|
+
import androidx.core.widget.TextViewCompat
|
|
26
|
+
import androidx.appcompat.R as AppCompatR
|
|
27
|
+
import com.facebook.react.bridge.WritableNativeMap
|
|
28
|
+
import com.facebook.react.uimanager.PixelUtil
|
|
29
|
+
import com.facebook.react.uimanager.ReactCompoundViewGroup
|
|
30
|
+
import com.facebook.react.uimanager.StateWrapper
|
|
31
|
+
import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper
|
|
32
|
+
import com.facebook.react.views.scroll.ReactScrollViewHelper
|
|
33
|
+
import com.google.android.material.textfield.TextInputEditText
|
|
34
|
+
import com.google.android.material.textfield.TextInputLayout
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A Material 3 text field: a TextInputLayout (floating label, box, icons,
|
|
38
|
+
* supporting text, counter) around a TextInputEditText. With
|
|
39
|
+
* `android.material: 'system'` it is the platform EditText instead (the hint
|
|
40
|
+
* as placeholder, compound-drawable icons, supporting text below), as the
|
|
41
|
+
* other components' system mode gives the AppCompat widget.
|
|
42
|
+
*
|
|
43
|
+
* The widgets are rebuilt for the props they read at construction (the box
|
|
44
|
+
* style, the native theme); everything else is applied to the live widgets.
|
|
45
|
+
* The text and its edit counter live here, so a rebuild keeps them.
|
|
46
|
+
*
|
|
47
|
+
* Text flows one way at a time: user edits report an event count with the
|
|
48
|
+
* text, and JS pushes a controlled value back with the last count it saw.
|
|
49
|
+
* A push that is older than the latest edit is dropped, so a slow JS
|
|
50
|
+
* round-trip can't erase what was typed meanwhile.
|
|
51
|
+
*/
|
|
52
|
+
class PCTextFieldView(context: Context) :
|
|
53
|
+
FrameLayout(context),
|
|
54
|
+
ReactScrollViewHelper.HasStateWrapper,
|
|
55
|
+
ReactCompoundViewGroup {
|
|
56
|
+
|
|
57
|
+
companion object {
|
|
58
|
+
private const val TAG = "PCTextField"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// --- State Wrapper for Fabric state updates ---
|
|
62
|
+
override var stateWrapper: StateWrapper? = null
|
|
63
|
+
|
|
64
|
+
private var lastReportedHeight: Float = -1f
|
|
65
|
+
|
|
66
|
+
// --- Props ---
|
|
67
|
+
var label: String = ""
|
|
68
|
+
var placeholder: String = ""
|
|
69
|
+
var supportingText: String = ""
|
|
70
|
+
var errorState: String = "none" // "none" | "error"
|
|
71
|
+
var errorText: String = ""
|
|
72
|
+
var prefix: String = ""
|
|
73
|
+
var suffix: String = ""
|
|
74
|
+
var leadingIcon: PCButtonSupport.Icon = PCButtonSupport.NO_ICON
|
|
75
|
+
var trailingIcon: PCButtonSupport.Icon = PCButtonSupport.NO_ICON
|
|
76
|
+
var clearButtonMode: String = "never"
|
|
77
|
+
var passwordToggle: Boolean = false
|
|
78
|
+
var characterCount: Boolean = false
|
|
79
|
+
var maxLength: Int = 0
|
|
80
|
+
var keyboardType: String = "default"
|
|
81
|
+
var returnKeyType: String = "default"
|
|
82
|
+
var autoCapitalize: String = "sentences"
|
|
83
|
+
var autoCorrect: Boolean = true
|
|
84
|
+
var secure: Boolean = false
|
|
85
|
+
var multiline: Boolean = false
|
|
86
|
+
var fieldEnabled: Boolean = true
|
|
87
|
+
var autoFocus: Boolean = false
|
|
88
|
+
var selectTextOnFocus: Boolean = false
|
|
89
|
+
var autoComplete: String = ""
|
|
90
|
+
var spokenLabel: String = ""
|
|
91
|
+
var fontFamily: String = ""
|
|
92
|
+
var fontSize: Float = 0f
|
|
93
|
+
var fontWeight: String = ""
|
|
94
|
+
var fontStyle: String = ""
|
|
95
|
+
var variant: String = "outlined" // "outlined" | "filled"
|
|
96
|
+
var dense: Boolean = false
|
|
97
|
+
var materialMode: String = "m3" // "m3" | "system"
|
|
98
|
+
|
|
99
|
+
// --- Events ---
|
|
100
|
+
var onChange: ((text: String, eventCount: Int) -> Unit)? = null
|
|
101
|
+
var onFocusChange: ((focused: Boolean, text: String) -> Unit)? = null
|
|
102
|
+
var onSubmit: ((text: String) -> Unit)? = null
|
|
103
|
+
var onTrailingIconPress: (() -> Unit)? = null
|
|
104
|
+
|
|
105
|
+
// --- Text state (survives rebuilds) ---
|
|
106
|
+
private var text: String = ""
|
|
107
|
+
private var nativeEventCount = 0
|
|
108
|
+
private var settingTextInternally = false
|
|
109
|
+
private var initialTextApplied = false
|
|
110
|
+
private var autoFocusDone = false
|
|
111
|
+
|
|
112
|
+
// --- UI ---
|
|
113
|
+
/** The top-level widget: the TextInputLayout, or the system field's column. */
|
|
114
|
+
private var widget: View? = null
|
|
115
|
+
private var layout: TextInputLayout? = null
|
|
116
|
+
private var editText: FieldEditText? = null
|
|
117
|
+
private var helperView: TextView? = null
|
|
118
|
+
|
|
119
|
+
/** The Material style's error icon, shown in the end slot while there is an error. */
|
|
120
|
+
private var defaultErrorIcon: Drawable? = null
|
|
121
|
+
|
|
122
|
+
/** Bumped on every rebuild and icon change so late image loads can't touch stale widgets. */
|
|
123
|
+
private var startIconGeneration = 0
|
|
124
|
+
private var endIconGeneration = 0
|
|
125
|
+
|
|
126
|
+
// --- Native theme ---
|
|
127
|
+
// Widgets read theme colors when they are created, so they are rebuilt when the
|
|
128
|
+
// native theme changes (brand color, light/dark switch).
|
|
129
|
+
private var builtThemeVersion = PCNativeTheme.version
|
|
130
|
+
private val nativeThemeListener = PCNativeTheme.Listener {
|
|
131
|
+
PCThemeSupport.clearColorStateListCaches(this)
|
|
132
|
+
rebuildUI()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Declared before init: rebuildUI() attaches the watcher, and Kotlin
|
|
136
|
+
// initializes properties in declaration order.
|
|
137
|
+
private val textWatcher = object : TextWatcher {
|
|
138
|
+
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
|
139
|
+
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
|
140
|
+
override fun afterTextChanged(s: Editable) {
|
|
141
|
+
val next = s.toString()
|
|
142
|
+
if (settingTextInternally) {
|
|
143
|
+
text = next
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
if (next == text) return
|
|
147
|
+
text = next
|
|
148
|
+
nativeEventCount += 1
|
|
149
|
+
onChange?.invoke(next, nativeEventCount)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private var manualLayoutPending = false
|
|
154
|
+
|
|
155
|
+
init {
|
|
156
|
+
clipChildren = false
|
|
157
|
+
clipToPadding = false
|
|
158
|
+
rebuildUI()
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---- Public apply* (called by manager) ----
|
|
162
|
+
|
|
163
|
+
/** The text the field starts with; only the first value is used (see setTextFromJS). */
|
|
164
|
+
fun applyInitialText(value: String) {
|
|
165
|
+
if (initialTextApplied) return
|
|
166
|
+
initialTextApplied = true
|
|
167
|
+
setTextInternal(value, moveCursorToEnd = true)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
fun applyLabel(value: String) {
|
|
171
|
+
if (label == value) return
|
|
172
|
+
label = value
|
|
173
|
+
applyHintAndPlaceholder()
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fun applyPlaceholder(value: String) {
|
|
177
|
+
if (placeholder == value) return
|
|
178
|
+
placeholder = value
|
|
179
|
+
applyHintAndPlaceholder()
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
fun applySupportingText(value: String) {
|
|
183
|
+
if (supportingText == value) return
|
|
184
|
+
supportingText = value
|
|
185
|
+
applyHelperAndError()
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
fun applyError(state: String, message: String) {
|
|
189
|
+
val nextState = if (state == "error") "error" else "none"
|
|
190
|
+
if (errorState == nextState && errorText == message) return
|
|
191
|
+
errorState = nextState
|
|
192
|
+
errorText = message
|
|
193
|
+
applyHelperAndError()
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
fun applyPrefix(value: String) {
|
|
197
|
+
if (prefix == value) return
|
|
198
|
+
prefix = value
|
|
199
|
+
layout?.prefixText = value.ifEmpty { null }
|
|
200
|
+
requestLayout()
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
fun applySuffix(value: String) {
|
|
204
|
+
if (suffix == value) return
|
|
205
|
+
suffix = value
|
|
206
|
+
layout?.suffixText = value.ifEmpty { null }
|
|
207
|
+
requestLayout()
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
fun applyLeadingIcon(value: PCButtonSupport.Icon) {
|
|
211
|
+
if (leadingIcon == value) return
|
|
212
|
+
leadingIcon = value
|
|
213
|
+
applyStartIcon()
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
fun applyTrailingIcon(value: PCButtonSupport.Icon) {
|
|
217
|
+
if (trailingIcon == value) return
|
|
218
|
+
trailingIcon = value
|
|
219
|
+
applyEndIcon()
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
fun applyClearButtonMode(value: String) {
|
|
223
|
+
if (clearButtonMode == value) return
|
|
224
|
+
clearButtonMode = value
|
|
225
|
+
applyEndIcon()
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
fun applyPasswordToggle(value: Boolean) {
|
|
229
|
+
if (passwordToggle == value) return
|
|
230
|
+
passwordToggle = value
|
|
231
|
+
applyEndIcon()
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
fun applyCharacterCount(value: Boolean) {
|
|
235
|
+
if (characterCount == value) return
|
|
236
|
+
characterCount = value
|
|
237
|
+
applyCounterAndLength()
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
fun applyMaxLength(value: Int) {
|
|
241
|
+
if (maxLength == value) return
|
|
242
|
+
maxLength = value
|
|
243
|
+
applyCounterAndLength()
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
fun applyKeyboardType(value: String) {
|
|
247
|
+
if (keyboardType == value) return
|
|
248
|
+
keyboardType = value
|
|
249
|
+
applyInputType()
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
fun applyReturnKeyType(value: String) {
|
|
253
|
+
if (returnKeyType == value) return
|
|
254
|
+
returnKeyType = value
|
|
255
|
+
applyInputType()
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
fun applyAutoCapitalize(value: String) {
|
|
259
|
+
if (autoCapitalize == value) return
|
|
260
|
+
autoCapitalize = value
|
|
261
|
+
applyInputType()
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
fun applyAutoCorrect(value: Boolean) {
|
|
265
|
+
if (autoCorrect == value) return
|
|
266
|
+
autoCorrect = value
|
|
267
|
+
applyInputType()
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
fun applySecure(value: Boolean) {
|
|
271
|
+
if (secure == value) return
|
|
272
|
+
secure = value
|
|
273
|
+
applyInputType()
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
fun applyMultiline(value: Boolean) {
|
|
277
|
+
if (multiline == value) return
|
|
278
|
+
multiline = value
|
|
279
|
+
applyInputType()
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
fun applyInteractivity(value: String?) {
|
|
283
|
+
val next = value != "disabled"
|
|
284
|
+
if (fieldEnabled == next) return
|
|
285
|
+
fieldEnabled = next
|
|
286
|
+
layout?.isEnabled = next
|
|
287
|
+
if (layout == null) editText?.isEnabled = next
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
fun applyAutoFocus(value: Boolean) {
|
|
291
|
+
autoFocus = value
|
|
292
|
+
if (value && isAttachedToWindow) performAutoFocus()
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
fun applySelectTextOnFocus(value: Boolean) {
|
|
296
|
+
if (selectTextOnFocus == value) return
|
|
297
|
+
selectTextOnFocus = value
|
|
298
|
+
editText?.setSelectAllOnFocus(value)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
fun applyAutoComplete(value: String) {
|
|
302
|
+
if (autoComplete == value) return
|
|
303
|
+
autoComplete = value
|
|
304
|
+
applyAutofill()
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
fun applySpokenLabel(value: String) {
|
|
308
|
+
if (spokenLabel == value) return
|
|
309
|
+
spokenLabel = value
|
|
310
|
+
editText?.contentDescription = value.ifEmpty { null }
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
fun applyTextStyle(family: String, size: Float, weight: String, style: String) {
|
|
314
|
+
if (fontFamily == family && fontSize == size && fontWeight == weight && fontStyle == style) return
|
|
315
|
+
fontFamily = family
|
|
316
|
+
fontSize = size
|
|
317
|
+
fontWeight = weight
|
|
318
|
+
fontStyle = style
|
|
319
|
+
editText?.let { applyFont(it) }
|
|
320
|
+
requestLayout()
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
fun applyVariant(value: String?) {
|
|
324
|
+
val next = if (value == "filled") "filled" else "outlined"
|
|
325
|
+
if (variant == next) return
|
|
326
|
+
variant = next
|
|
327
|
+
rebuildUI()
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
fun applyDensity(value: String?) {
|
|
331
|
+
val next = value == "dense"
|
|
332
|
+
if (dense == next) return
|
|
333
|
+
dense = next
|
|
334
|
+
rebuildUI()
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
fun applyMaterial(value: String?) {
|
|
338
|
+
val next = if (value == "system") "system" else "m3"
|
|
339
|
+
if (materialMode == next) return
|
|
340
|
+
materialMode = next
|
|
341
|
+
rebuildUI()
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ---- Commands ----
|
|
345
|
+
|
|
346
|
+
fun focusFromJS() {
|
|
347
|
+
val edit = editText ?: return
|
|
348
|
+
edit.requestFocus()
|
|
349
|
+
if (edit.isInTouchMode) showKeyboard(edit)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
fun blurFromJS() {
|
|
353
|
+
val edit = editText ?: return
|
|
354
|
+
clearFocusAndMaybeRefocus(edit)
|
|
355
|
+
hideKeyboard()
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Clears the text as if the user had, so JS gets a change event. */
|
|
359
|
+
fun clearFromJS() {
|
|
360
|
+
val edit = editText ?: return
|
|
361
|
+
if (edit.length() == 0) return
|
|
362
|
+
edit.setText("")
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* A controlled value from JS. Dropped when the user has edited since
|
|
367
|
+
* [eventCount] was reported: the edit event that follows carries the text
|
|
368
|
+
* JS should reconcile against.
|
|
369
|
+
*/
|
|
370
|
+
fun setTextFromJS(eventCount: Int, value: String) {
|
|
371
|
+
if (eventCount < nativeEventCount) return
|
|
372
|
+
if (value == text) return
|
|
373
|
+
setTextInternal(value, moveCursorToEnd = false)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ---- Touch ----
|
|
377
|
+
|
|
378
|
+
// The widgets have generated view ids, which React Native's touch handling
|
|
379
|
+
// would take for React tags and dispatch JS touch events to unrelated views.
|
|
380
|
+
// Claim the touch for this view; the field still receives it natively.
|
|
381
|
+
override fun interceptsTouchEvent(touchX: Float, touchY: Float): Boolean = true
|
|
382
|
+
|
|
383
|
+
override fun reactTagForTouch(touchX: Float, touchY: Float): Int = id
|
|
384
|
+
|
|
385
|
+
// ---- Lifecycle ----
|
|
386
|
+
|
|
387
|
+
override fun onAttachedToWindow() {
|
|
388
|
+
super.onAttachedToWindow()
|
|
389
|
+
PCNativeTheme.addListener(nativeThemeListener)
|
|
390
|
+
PCNativeTheme.attach(context)
|
|
391
|
+
if (builtThemeVersion != PCNativeTheme.version) {
|
|
392
|
+
nativeThemeListener.onNativeThemeChanged()
|
|
393
|
+
}
|
|
394
|
+
if (autoFocus) performAutoFocus()
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
override fun onDetachedFromWindow() {
|
|
398
|
+
PCNativeTheme.removeListener(nativeThemeListener)
|
|
399
|
+
super.onDetachedFromWindow()
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private fun performAutoFocus() {
|
|
403
|
+
if (autoFocusDone) return
|
|
404
|
+
autoFocusDone = true
|
|
405
|
+
post { focusFromJS() }
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ---- UI Building ----
|
|
409
|
+
|
|
410
|
+
private fun rebuildUI() {
|
|
411
|
+
builtThemeVersion = PCNativeTheme.version
|
|
412
|
+
val wasFocused = editText?.isFocused == true
|
|
413
|
+
val selection = editText?.selectionEnd ?: -1
|
|
414
|
+
removeAllViews()
|
|
415
|
+
startIconGeneration += 1
|
|
416
|
+
endIconGeneration += 1
|
|
417
|
+
layout = null
|
|
418
|
+
helperView = null
|
|
419
|
+
|
|
420
|
+
val edit: FieldEditText
|
|
421
|
+
if (materialMode == "system") {
|
|
422
|
+
// The platform field: an AppCompat EditText with a supporting-text line
|
|
423
|
+
val column = LinearLayout(context).apply {
|
|
424
|
+
orientation = LinearLayout.VERTICAL
|
|
425
|
+
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
|
426
|
+
}
|
|
427
|
+
edit = FieldEditText(context).apply {
|
|
428
|
+
id = View.generateViewId()
|
|
429
|
+
layoutParams = LinearLayout.LayoutParams(
|
|
430
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
431
|
+
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
432
|
+
)
|
|
433
|
+
compoundDrawablePadding = dp(8)
|
|
434
|
+
setSelectAllOnFocus(selectTextOnFocus)
|
|
435
|
+
contentDescription = spokenLabel.ifEmpty { null }
|
|
436
|
+
isEnabled = fieldEnabled
|
|
437
|
+
}
|
|
438
|
+
val helper = TextView(context).apply {
|
|
439
|
+
layoutParams = LinearLayout.LayoutParams(
|
|
440
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
441
|
+
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
442
|
+
).apply { marginStart = dp(4); topMargin = dp(2) }
|
|
443
|
+
TextViewCompat.setTextAppearance(this, android.R.style.TextAppearance_Material_Small)
|
|
444
|
+
visibility = View.GONE
|
|
445
|
+
}
|
|
446
|
+
column.addView(edit)
|
|
447
|
+
column.addView(helper)
|
|
448
|
+
addView(column)
|
|
449
|
+
widget = column
|
|
450
|
+
helperView = helper
|
|
451
|
+
editText = edit
|
|
452
|
+
} else {
|
|
453
|
+
// Material widgets need a Material theme; fall back to Material 3 defaults instead of crashing.
|
|
454
|
+
val base = PCThemeSupport.materialContext(context, "TextField")
|
|
455
|
+
val styleOverlay = when {
|
|
456
|
+
variant == "filled" && dense -> R.style.PCTextField_Filled_Dense
|
|
457
|
+
variant == "filled" -> R.style.PCTextField_Filled
|
|
458
|
+
dense -> R.style.PCTextField_Outlined_Dense
|
|
459
|
+
else -> R.style.PCTextField_Outlined
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
val til = TextInputLayout(ContextThemeWrapper(base, styleOverlay)).apply {
|
|
463
|
+
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// The layout's context carries the style's theme overlay for its edit text
|
|
467
|
+
edit = FieldEditText(til.context).apply {
|
|
468
|
+
id = View.generateViewId()
|
|
469
|
+
layoutParams = LinearLayout.LayoutParams(
|
|
470
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
471
|
+
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
472
|
+
)
|
|
473
|
+
setSelectAllOnFocus(selectTextOnFocus)
|
|
474
|
+
contentDescription = spokenLabel.ifEmpty { null }
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
til.addView(edit)
|
|
478
|
+
addView(til)
|
|
479
|
+
widget = til
|
|
480
|
+
layout = til
|
|
481
|
+
editText = edit
|
|
482
|
+
defaultErrorIcon = til.errorIconDrawable
|
|
483
|
+
til.prefixText = prefix.ifEmpty { null }
|
|
484
|
+
til.suffixText = suffix.ifEmpty { null }
|
|
485
|
+
til.isEnabled = fieldEnabled
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
applyInputType()
|
|
489
|
+
applyHintAndPlaceholder()
|
|
490
|
+
applyHelperAndError()
|
|
491
|
+
applyStartIcon()
|
|
492
|
+
applyEndIcon()
|
|
493
|
+
applyCounterAndLength()
|
|
494
|
+
applyAutofill()
|
|
495
|
+
|
|
496
|
+
// Restore the text without reporting it as an edit
|
|
497
|
+
settingTextInternally = true
|
|
498
|
+
edit.setText(text)
|
|
499
|
+
val length = edit.length()
|
|
500
|
+
edit.setSelection(if (selection in 0..length) selection else length)
|
|
501
|
+
settingTextInternally = false
|
|
502
|
+
|
|
503
|
+
edit.addTextChangedListener(textWatcher)
|
|
504
|
+
// Focus changes are reported from FieldEditText.onFocusChanged: the Material
|
|
505
|
+
// end-icon delegates (clear text, for one) install their own
|
|
506
|
+
// OnFocusChangeListener on the edit text, replacing any listener set here.
|
|
507
|
+
edit.setOnEditorActionListener { _, actionId, event -> onEditorAction(actionId, event) }
|
|
508
|
+
|
|
509
|
+
if (wasFocused) edit.requestFocus()
|
|
510
|
+
requestLayout()
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private fun onEditorAction(actionId: Int, event: KeyEvent?): Boolean {
|
|
514
|
+
// Multi-line fields keep the return key for newlines
|
|
515
|
+
if (multiline) return false
|
|
516
|
+
// A hardware / IME Enter reports IME_ACTION_UNSPECIFIED twice (key down and up)
|
|
517
|
+
if (actionId == EditorInfo.IME_ACTION_UNSPECIFIED && event != null && event.action != KeyEvent.ACTION_DOWN) {
|
|
518
|
+
return true
|
|
519
|
+
}
|
|
520
|
+
onSubmit?.invoke(text)
|
|
521
|
+
// Single-line fields blur on submit, as the core TextInput does by default
|
|
522
|
+
blurFromJS()
|
|
523
|
+
return true
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
private fun setTextInternal(value: String, moveCursorToEnd: Boolean) {
|
|
527
|
+
val edit = editText
|
|
528
|
+
if (edit == null) {
|
|
529
|
+
text = value
|
|
530
|
+
return
|
|
531
|
+
}
|
|
532
|
+
settingTextInternally = true
|
|
533
|
+
val wasAtEnd = edit.selectionEnd == edit.length()
|
|
534
|
+
val cursor = edit.selectionStart
|
|
535
|
+
edit.setText(value)
|
|
536
|
+
text = value
|
|
537
|
+
val length = edit.length()
|
|
538
|
+
edit.setSelection(if (moveCursorToEnd || wasAtEnd) length else cursor.coerceIn(0, length))
|
|
539
|
+
settingTextInternally = false
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// ---- Decorations ----
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* With a label the Material layout floats it and shows the placeholder
|
|
546
|
+
* while focused; without one the placeholder is the edit text's own hint,
|
|
547
|
+
* and the box shrinks to the height of a plain field.
|
|
548
|
+
*/
|
|
549
|
+
private fun applyHintAndPlaceholder() {
|
|
550
|
+
val edit = editText ?: return
|
|
551
|
+
val til = layout
|
|
552
|
+
if (til == null) {
|
|
553
|
+
// The platform field has one hint: the placeholder, else the label
|
|
554
|
+
edit.hint = placeholder.ifEmpty { label }.ifEmpty { null }
|
|
555
|
+
requestLayout()
|
|
556
|
+
return
|
|
557
|
+
}
|
|
558
|
+
if (label.isNotEmpty()) {
|
|
559
|
+
edit.hint = null
|
|
560
|
+
til.isHintEnabled = true
|
|
561
|
+
til.hint = label
|
|
562
|
+
til.placeholderText = placeholder.ifEmpty { null }
|
|
563
|
+
} else {
|
|
564
|
+
til.placeholderText = null
|
|
565
|
+
til.hint = null
|
|
566
|
+
til.isHintEnabled = false
|
|
567
|
+
edit.hint = placeholder.ifEmpty { null }
|
|
568
|
+
}
|
|
569
|
+
requestLayout()
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
private fun applyHelperAndError() {
|
|
573
|
+
val til = layout
|
|
574
|
+
if (til == null) {
|
|
575
|
+
applySystemHelper()
|
|
576
|
+
return
|
|
577
|
+
}
|
|
578
|
+
til.helperText = supportingText.ifEmpty { null }
|
|
579
|
+
if (errorState == "error") {
|
|
580
|
+
// No message: the supporting text turns to the error color, as the
|
|
581
|
+
// Material spec shows; with nothing to show, keep the outline red only
|
|
582
|
+
til.isErrorEnabled = true
|
|
583
|
+
til.error = errorText.ifEmpty { supportingText.ifEmpty { " " } }
|
|
584
|
+
} else {
|
|
585
|
+
til.error = null
|
|
586
|
+
til.isErrorEnabled = false
|
|
587
|
+
}
|
|
588
|
+
requestLayout()
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/** The system field's supporting / error line, in the theme's secondary or error color. */
|
|
592
|
+
private fun applySystemHelper() {
|
|
593
|
+
val helper = helperView ?: return
|
|
594
|
+
val showsError = errorState == "error"
|
|
595
|
+
val message = if (showsError && errorText.isNotEmpty()) errorText else supportingText
|
|
596
|
+
helper.text = message
|
|
597
|
+
helper.visibility = if (message.isEmpty()) View.GONE else View.VISIBLE
|
|
598
|
+
val color = if (showsError) {
|
|
599
|
+
themeColor(AppCompatR.attr.colorError, 0xFFB3261E.toInt())
|
|
600
|
+
} else {
|
|
601
|
+
themeColor(android.R.attr.textColorSecondary, 0xFF757575.toInt())
|
|
602
|
+
}
|
|
603
|
+
helper.setTextColor(color)
|
|
604
|
+
requestLayout()
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
private fun themeColor(attr: Int, fallback: Int): Int {
|
|
608
|
+
val value = TypedValue()
|
|
609
|
+
if (!context.theme.resolveAttribute(attr, value, true)) return fallback
|
|
610
|
+
return if (value.resourceId != 0) context.getColor(value.resourceId) else value.data
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
private fun dp(value: Int): Int = (value * resources.displayMetrics.density).toInt()
|
|
614
|
+
|
|
615
|
+
/** Icons of the system field: compound drawables, the trailing one tappable. */
|
|
616
|
+
private var systemStart: Drawable? = null
|
|
617
|
+
private var systemEnd: Drawable? = null
|
|
618
|
+
|
|
619
|
+
private fun applySystemIcons() {
|
|
620
|
+
val edit = editText ?: return
|
|
621
|
+
val start = systemStart
|
|
622
|
+
val end = systemEnd
|
|
623
|
+
edit.setCompoundDrawablesRelativeWithIntrinsicBounds(start, null, end, null)
|
|
624
|
+
val tinted = (start != null && leadingIcon.tinted) || (end != null && trailingIcon.tinted)
|
|
625
|
+
TextViewCompat.setCompoundDrawableTintList(
|
|
626
|
+
edit,
|
|
627
|
+
if (tinted) ColorStateList.valueOf(themeColor(android.R.attr.colorControlNormal, 0xFF757575.toInt())) else null
|
|
628
|
+
)
|
|
629
|
+
if (end != null) {
|
|
630
|
+
edit.setOnTouchListener { v, event ->
|
|
631
|
+
if (event.action == MotionEvent.ACTION_UP) {
|
|
632
|
+
val width = end.bounds.width() + edit.compoundDrawablePadding
|
|
633
|
+
val isRtl = v.layoutDirection == View.LAYOUT_DIRECTION_RTL
|
|
634
|
+
val hit = if (isRtl) event.x <= v.paddingStart + width else event.x >= v.width - v.paddingEnd - width
|
|
635
|
+
if (hit) {
|
|
636
|
+
v.performClick()
|
|
637
|
+
onTrailingIconPress?.invoke()
|
|
638
|
+
return@setOnTouchListener true
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
false
|
|
642
|
+
}
|
|
643
|
+
} else {
|
|
644
|
+
edit.setOnTouchListener(null)
|
|
645
|
+
}
|
|
646
|
+
requestLayout()
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
private fun applyStartIcon() {
|
|
650
|
+
startIconGeneration += 1
|
|
651
|
+
val generation = startIconGeneration
|
|
652
|
+
val til = layout
|
|
653
|
+
if (til == null) {
|
|
654
|
+
systemStart = null
|
|
655
|
+
if (leadingIcon.isPresent) {
|
|
656
|
+
loadIcon(leadingIcon) { drawable ->
|
|
657
|
+
if (generation != startIconGeneration || layout != null) return@loadIcon
|
|
658
|
+
systemStart = drawable
|
|
659
|
+
applySystemIcons()
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
applySystemIcons()
|
|
663
|
+
return
|
|
664
|
+
}
|
|
665
|
+
if (!leadingIcon.isPresent) {
|
|
666
|
+
til.startIconDrawable = null
|
|
667
|
+
requestLayout()
|
|
668
|
+
return
|
|
669
|
+
}
|
|
670
|
+
loadIcon(leadingIcon) { drawable ->
|
|
671
|
+
if (generation != startIconGeneration || layout !== til) return@loadIcon
|
|
672
|
+
if (!leadingIcon.tinted) til.setStartIconTintList(null)
|
|
673
|
+
til.startIconDrawable = drawable
|
|
674
|
+
requestLayout()
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* The end icon slot, by priority: a custom trailing icon, the password
|
|
680
|
+
* toggle, the clear button (shown while the focused field has text).
|
|
681
|
+
*/
|
|
682
|
+
private fun applyEndIcon() {
|
|
683
|
+
endIconGeneration += 1
|
|
684
|
+
val generation = endIconGeneration
|
|
685
|
+
val til = layout
|
|
686
|
+
if (til == null) {
|
|
687
|
+
// The platform field has no clear button or password toggle
|
|
688
|
+
systemEnd = null
|
|
689
|
+
if (trailingIcon.isPresent) {
|
|
690
|
+
loadIcon(trailingIcon) { drawable ->
|
|
691
|
+
if (generation != endIconGeneration || layout != null) return@loadIcon
|
|
692
|
+
systemEnd = drawable
|
|
693
|
+
applySystemIcons()
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
applySystemIcons()
|
|
697
|
+
return
|
|
698
|
+
}
|
|
699
|
+
when {
|
|
700
|
+
trailingIcon.isPresent -> {
|
|
701
|
+
til.endIconMode = TextInputLayout.END_ICON_CUSTOM
|
|
702
|
+
til.setEndIconOnClickListener { onTrailingIconPress?.invoke() }
|
|
703
|
+
loadIcon(trailingIcon) { drawable ->
|
|
704
|
+
if (generation != endIconGeneration || layout !== til) return@loadIcon
|
|
705
|
+
if (!trailingIcon.tinted) til.setEndIconTintList(null)
|
|
706
|
+
til.endIconDrawable = drawable
|
|
707
|
+
requestLayout()
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
passwordToggle -> til.endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE
|
|
711
|
+
clearButtonMode != "never" -> til.endIconMode = TextInputLayout.END_ICON_CLEAR_TEXT
|
|
712
|
+
else -> til.endIconMode = TextInputLayout.END_ICON_NONE
|
|
713
|
+
}
|
|
714
|
+
// Material replaces the end icon with its error icon while an error shows;
|
|
715
|
+
// a password toggle or a custom icon stays, since the user still needs it
|
|
716
|
+
til.errorIconDrawable = if (trailingIcon.isPresent || passwordToggle) null else defaultErrorIcon
|
|
717
|
+
requestLayout()
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
private fun applyCounterAndLength() {
|
|
721
|
+
val edit = editText ?: return
|
|
722
|
+
edit.filters = if (maxLength > 0) arrayOf(InputFilter.LengthFilter(maxLength)) else emptyArray()
|
|
723
|
+
val til = layout ?: return
|
|
724
|
+
til.counterMaxLength = maxLength
|
|
725
|
+
til.isCounterEnabled = characterCount
|
|
726
|
+
requestLayout()
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
private fun applyInputType() {
|
|
730
|
+
val edit = editText ?: return
|
|
731
|
+
|
|
732
|
+
var type = when (keyboardType) {
|
|
733
|
+
"numeric" -> InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL or InputType.TYPE_NUMBER_FLAG_SIGNED
|
|
734
|
+
"number-pad" -> InputType.TYPE_CLASS_NUMBER
|
|
735
|
+
"decimal-pad" -> InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
|
|
736
|
+
"phone-pad" -> InputType.TYPE_CLASS_PHONE
|
|
737
|
+
"email-address" -> InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
|
|
738
|
+
"url" -> InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI
|
|
739
|
+
"visible-password" -> InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
|
|
740
|
+
else -> InputType.TYPE_CLASS_TEXT
|
|
741
|
+
}
|
|
742
|
+
val isText = (type and InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT
|
|
743
|
+
val isNumber = (type and InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_NUMBER
|
|
744
|
+
|
|
745
|
+
if (secure) {
|
|
746
|
+
if (isText) {
|
|
747
|
+
type = (type and InputType.TYPE_MASK_VARIATION.inv()) or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
|
748
|
+
} else if (isNumber) {
|
|
749
|
+
type = type or InputType.TYPE_NUMBER_VARIATION_PASSWORD
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (isText && !secure) {
|
|
754
|
+
type = type or when (autoCapitalize) {
|
|
755
|
+
"characters" -> InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
|
|
756
|
+
"words" -> InputType.TYPE_TEXT_FLAG_CAP_WORDS
|
|
757
|
+
"sentences" -> InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
|
|
758
|
+
else -> 0
|
|
759
|
+
}
|
|
760
|
+
type = type or if (autoCorrect) {
|
|
761
|
+
InputType.TYPE_TEXT_FLAG_AUTO_CORRECT
|
|
762
|
+
} else {
|
|
763
|
+
InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (multiline && isText) {
|
|
768
|
+
type = type or InputType.TYPE_TEXT_FLAG_MULTI_LINE
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
if (edit.inputType != type) {
|
|
772
|
+
// Setting the input type resets the typeface (password fields default
|
|
773
|
+
// to monospace) and the transformation; the font is re-applied below.
|
|
774
|
+
edit.inputType = type
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
edit.imeOptions = when (returnKeyType) {
|
|
778
|
+
"done" -> EditorInfo.IME_ACTION_DONE
|
|
779
|
+
"go" -> EditorInfo.IME_ACTION_GO
|
|
780
|
+
"next" -> EditorInfo.IME_ACTION_NEXT
|
|
781
|
+
"search" -> EditorInfo.IME_ACTION_SEARCH
|
|
782
|
+
"send" -> EditorInfo.IME_ACTION_SEND
|
|
783
|
+
"previous" -> EditorInfo.IME_ACTION_PREVIOUS
|
|
784
|
+
"none" -> EditorInfo.IME_ACTION_NONE
|
|
785
|
+
else -> EditorInfo.IME_ACTION_UNSPECIFIED
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (multiline) {
|
|
789
|
+
edit.gravity = Gravity.TOP or Gravity.START
|
|
790
|
+
edit.minLines = 1
|
|
791
|
+
} else {
|
|
792
|
+
edit.gravity = Gravity.CENTER_VERTICAL or Gravity.START
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
applyFont(edit)
|
|
796
|
+
requestLayout()
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
private fun applyFont(edit: TextInputEditText) {
|
|
800
|
+
PCButtonSupport.applyFont(edit, fontFamily, fontSize, fontWeight, fontStyle)
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
private fun applyAutofill() {
|
|
804
|
+
val edit = editText ?: return
|
|
805
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
|
806
|
+
when (autoComplete) {
|
|
807
|
+
"" -> {
|
|
808
|
+
edit.importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_AUTO
|
|
809
|
+
edit.setAutofillHints()
|
|
810
|
+
}
|
|
811
|
+
"off" -> {
|
|
812
|
+
edit.importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_NO
|
|
813
|
+
edit.setAutofillHints()
|
|
814
|
+
}
|
|
815
|
+
else -> {
|
|
816
|
+
val hint = autofillHint(autoComplete)
|
|
817
|
+
edit.importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_YES
|
|
818
|
+
if (hint != null) edit.setAutofillHints(hint) else edit.setAutofillHints()
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/** React Native autoComplete values onto the Android autofill hints. */
|
|
824
|
+
private fun autofillHint(value: String): String? = when (value) {
|
|
825
|
+
"username" -> View.AUTOFILL_HINT_USERNAME
|
|
826
|
+
"password" -> View.AUTOFILL_HINT_PASSWORD
|
|
827
|
+
"new-password" -> "newPassword" // HintConstants.AUTOFILL_HINT_NEW_PASSWORD
|
|
828
|
+
"one-time-code" -> "smsOTPCode" // HintConstants.AUTOFILL_HINT_SMS_OTP
|
|
829
|
+
"email" -> View.AUTOFILL_HINT_EMAIL_ADDRESS
|
|
830
|
+
"name" -> View.AUTOFILL_HINT_NAME
|
|
831
|
+
"given-name" -> "personGivenName" // HintConstants.AUTOFILL_HINT_PERSON_NAME_GIVEN
|
|
832
|
+
"family-name" -> "personFamilyName" // HintConstants.AUTOFILL_HINT_PERSON_NAME_FAMILY
|
|
833
|
+
"tel" -> View.AUTOFILL_HINT_PHONE
|
|
834
|
+
"street-address" -> View.AUTOFILL_HINT_POSTAL_ADDRESS
|
|
835
|
+
"postal-code" -> View.AUTOFILL_HINT_POSTAL_CODE
|
|
836
|
+
"country" -> "addressCountry" // HintConstants.AUTOFILL_HINT_POSTAL_ADDRESS_COUNTRY
|
|
837
|
+
"cc-number" -> View.AUTOFILL_HINT_CREDIT_CARD_NUMBER
|
|
838
|
+
"cc-exp" -> View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE
|
|
839
|
+
"cc-csc" -> View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE
|
|
840
|
+
else -> null
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Resolves an icon. Drawable names and release-bundled assets (which React
|
|
845
|
+
* Native ships as drawable resources) resolve synchronously; other image
|
|
846
|
+
* URIs load in the background and are delivered when they arrive.
|
|
847
|
+
*/
|
|
848
|
+
private fun loadIcon(icon: PCButtonSupport.Icon, onLoaded: (Drawable?) -> Unit) {
|
|
849
|
+
when (icon.type) {
|
|
850
|
+
"drawable" -> onLoaded(ResourceDrawableIdHelper.instance.getResourceDrawable(context, icon.name))
|
|
851
|
+
"image" -> {
|
|
852
|
+
val uri = icon.uri
|
|
853
|
+
if (!uri.contains(':')) {
|
|
854
|
+
onLoaded(ResourceDrawableIdHelper.instance.getResourceDrawable(context, uri))
|
|
855
|
+
} else {
|
|
856
|
+
PCImageLoader.load(context, uri, icon.scale) { bitmap ->
|
|
857
|
+
onLoaded(bitmap?.let { BitmapDrawable(context.resources, it) })
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
else -> onLoaded(null)
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// ---- Keyboard and focus ----
|
|
866
|
+
|
|
867
|
+
private fun showKeyboard(view: View) {
|
|
868
|
+
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
|
869
|
+
imm?.showSoftInput(view, 0)
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
private fun hideKeyboard() {
|
|
873
|
+
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
|
874
|
+
imm?.hideSoftInputFromWindow(windowToken, 0)
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Drops focus. Android 9 and below would hand it to the next focusable
|
|
879
|
+
* view (often another text field) when the root view refocuses; block
|
|
880
|
+
* that, as the core TextInput does.
|
|
881
|
+
*/
|
|
882
|
+
private fun clearFocusAndMaybeRefocus(edit: View) {
|
|
883
|
+
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P || !edit.isInTouchMode) {
|
|
884
|
+
edit.clearFocus()
|
|
885
|
+
return
|
|
886
|
+
}
|
|
887
|
+
val root = rootView as? ViewGroup
|
|
888
|
+
if (root == null) {
|
|
889
|
+
edit.clearFocus()
|
|
890
|
+
return
|
|
891
|
+
}
|
|
892
|
+
val previous = root.descendantFocusability
|
|
893
|
+
root.descendantFocusability = ViewGroup.FOCUS_BLOCK_DESCENDANTS
|
|
894
|
+
edit.clearFocus()
|
|
895
|
+
root.descendantFocusability = previous
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* The edit text. The keyboard's back key hides the keyboard; the field
|
|
900
|
+
* drops focus with it so JS gets onBlur, as the core TextInput does not,
|
|
901
|
+
* but a Material field with a focused label and no keyboard looks stuck.
|
|
902
|
+
*/
|
|
903
|
+
private inner class FieldEditText(context: Context) : TextInputEditText(context) {
|
|
904
|
+
override fun onFocusChanged(focused: Boolean, direction: Int, previouslyFocusedRect: Rect?) {
|
|
905
|
+
super.onFocusChanged(focused, direction, previouslyFocusedRect)
|
|
906
|
+
onFocusChange?.invoke(focused, this@PCTextFieldView.text)
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
override fun onKeyPreIme(keyCode: Int, event: KeyEvent): Boolean {
|
|
910
|
+
if (keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP && isFocused) {
|
|
911
|
+
clearFocusAndMaybeRefocus(this)
|
|
912
|
+
}
|
|
913
|
+
return super.onKeyPreIme(keyCode, event)
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// ---- Layout ----
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* React Native's root view ignores requestLayout after the initial pass, so
|
|
921
|
+
* a native view whose subtree changes later (a longer multi-line text, an
|
|
922
|
+
* error message appearing) has to measure and lay itself out. Coalesced to
|
|
923
|
+
* one pass per frame.
|
|
924
|
+
*/
|
|
925
|
+
override fun requestLayout() {
|
|
926
|
+
super.requestLayout()
|
|
927
|
+
if (!manualLayoutPending) {
|
|
928
|
+
manualLayoutPending = true
|
|
929
|
+
post { runManualLayout() }
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
private fun runManualLayout() {
|
|
934
|
+
manualLayoutPending = false
|
|
935
|
+
// Nothing to do until Yoga has placed us; the regular pass covers that.
|
|
936
|
+
if (width == 0 || height == 0) return
|
|
937
|
+
measure(
|
|
938
|
+
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
|
939
|
+
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
|
|
940
|
+
)
|
|
941
|
+
layout(left, top, right, bottom)
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// ---- Measurement ----
|
|
945
|
+
|
|
946
|
+
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
947
|
+
val child = widget
|
|
948
|
+
if (child == null) {
|
|
949
|
+
setMeasuredDimension(0, 0)
|
|
950
|
+
return
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// The field fills the width it is given and takes the height its
|
|
954
|
+
// content needs; Yoga catches up through the state update.
|
|
955
|
+
val width = resolveWidth(widthMeasureSpec)
|
|
956
|
+
measureWidget(child, width)
|
|
957
|
+
val naturalHeight = child.measuredHeight
|
|
958
|
+
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
|
|
959
|
+
val height = when (MeasureSpec.getMode(heightMeasureSpec)) {
|
|
960
|
+
// Fabric may give us 0 before the state is applied; keep the natural size then.
|
|
961
|
+
MeasureSpec.EXACTLY -> if (heightSize > 0) heightSize else naturalHeight
|
|
962
|
+
MeasureSpec.AT_MOST -> minOf(naturalHeight, heightSize)
|
|
963
|
+
else -> naturalHeight
|
|
964
|
+
}
|
|
965
|
+
setMeasuredDimension(width, height)
|
|
966
|
+
reportHeight(naturalHeight)
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/** Measures the widget at [width], as tall as its content needs. */
|
|
970
|
+
private fun measureWidget(child: View, width: Int) {
|
|
971
|
+
child.measure(
|
|
972
|
+
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
|
973
|
+
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
|
|
974
|
+
)
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
private fun resolveWidth(spec: Int): Int {
|
|
978
|
+
val size = MeasureSpec.getSize(spec)
|
|
979
|
+
return if (size > 0) size else (PCConstants.FALLBACK_WIDTH_DP * resources.displayMetrics.density).toInt()
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
983
|
+
val child = widget ?: return
|
|
984
|
+
// Keep the widget at its natural height even when Yoga hasn't caught up
|
|
985
|
+
child.layout(0, 0, right - left, child.measuredHeight)
|
|
986
|
+
// TextInputLayout makes room for its start icon and prefix (invisible
|
|
987
|
+
// compound drawables on the edit text) from a ViewTreeObserver global
|
|
988
|
+
// layout callback, which only a window traversal delivers. React Native's
|
|
989
|
+
// root doesn't run one for a native subtree that laid itself out, so run
|
|
990
|
+
// it here; when it changes something it asks for another pass.
|
|
991
|
+
layout?.onGlobalLayout()
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/** Reports the intrinsic height to Fabric; called as soon as the state wrapper is available. */
|
|
995
|
+
fun reportIntrinsicSize() {
|
|
996
|
+
val child = widget ?: return
|
|
997
|
+
val width = if (width > 0) width else (PCConstants.FALLBACK_WIDTH_DP * resources.displayMetrics.density).toInt()
|
|
998
|
+
measureWidget(child, width)
|
|
999
|
+
reportHeight(child.measuredHeight)
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/**
|
|
1003
|
+
* Update Fabric state with the field's natural height. The width stays 0:
|
|
1004
|
+
* the shadow node fills the available width.
|
|
1005
|
+
*/
|
|
1006
|
+
private fun reportHeight(heightPx: Int) {
|
|
1007
|
+
val wrapper = stateWrapper ?: return
|
|
1008
|
+
val heightDp = PixelUtil.toDIPFromPixel(heightPx.toFloat())
|
|
1009
|
+
if (heightDp == lastReportedHeight) return
|
|
1010
|
+
lastReportedHeight = heightDp
|
|
1011
|
+
|
|
1012
|
+
val stateData = WritableNativeMap().apply {
|
|
1013
|
+
putDouble("width", 0.0)
|
|
1014
|
+
putDouble("height", heightDp.toDouble())
|
|
1015
|
+
}
|
|
1016
|
+
wrapper.updateState(stateData)
|
|
1017
|
+
}
|
|
1018
|
+
}
|