@onekeyfe/react-native-auto-size-input 1.1.28 → 1.1.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/com/margelo/nitro/autosizeinput/AutoSizeInput.kt +242 -67
- package/ios/AutoSizeInput.swift +75 -4
- package/lib/nitrogen/generated/android/c++/JHybridAutoSizeInputSpec.cpp +27 -0
- package/lib/nitrogen/generated/android/c++/JHybridAutoSizeInputSpec.hpp +6 -0
- package/lib/nitrogen/generated/android/c++/views/JHybridAutoSizeInputStateUpdater.cpp +12 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/autosizeinput/HybridAutoSizeInputSpec.kt +18 -0
- package/lib/nitrogen/generated/ios/c++/HybridAutoSizeInputSpecSwift.hpp +21 -0
- package/lib/nitrogen/generated/ios/c++/views/HybridAutoSizeInputComponent.mm +15 -0
- package/lib/nitrogen/generated/ios/swift/HybridAutoSizeInputSpec.swift +3 -0
- package/lib/nitrogen/generated/ios/swift/HybridAutoSizeInputSpec_cxx.swift +72 -0
- package/lib/nitrogen/generated/shared/c++/HybridAutoSizeInputSpec.cpp +6 -0
- package/lib/nitrogen/generated/shared/c++/HybridAutoSizeInputSpec.hpp +6 -0
- package/lib/nitrogen/generated/shared/c++/views/HybridAutoSizeInputComponent.cpp +36 -0
- package/lib/nitrogen/generated/shared/c++/views/HybridAutoSizeInputComponent.hpp +3 -0
- package/lib/nitrogen/generated/shared/json/AutoSizeInputConfig.json +3 -0
- package/lib/typescript/src/AutoSizeInput.nitro.d.ts +3 -0
- package/lib/typescript/src/AutoSizeInput.nitro.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JHybridAutoSizeInputSpec.cpp +27 -0
- package/nitrogen/generated/android/c++/JHybridAutoSizeInputSpec.hpp +6 -0
- package/nitrogen/generated/android/c++/views/JHybridAutoSizeInputStateUpdater.cpp +12 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/autosizeinput/HybridAutoSizeInputSpec.kt +18 -0
- package/nitrogen/generated/ios/c++/HybridAutoSizeInputSpecSwift.hpp +21 -0
- package/nitrogen/generated/ios/c++/views/HybridAutoSizeInputComponent.mm +15 -0
- package/nitrogen/generated/ios/swift/HybridAutoSizeInputSpec.swift +3 -0
- package/nitrogen/generated/ios/swift/HybridAutoSizeInputSpec_cxx.swift +72 -0
- package/nitrogen/generated/shared/c++/HybridAutoSizeInputSpec.cpp +6 -0
- package/nitrogen/generated/shared/c++/HybridAutoSizeInputSpec.hpp +6 -0
- package/nitrogen/generated/shared/c++/views/HybridAutoSizeInputComponent.cpp +36 -0
- package/nitrogen/generated/shared/c++/views/HybridAutoSizeInputComponent.hpp +3 -0
- package/nitrogen/generated/shared/json/AutoSizeInputConfig.json +3 -0
- package/package.json +1 -1
- package/src/AutoSizeInput.nitro.ts +7 -0
|
@@ -2,7 +2,9 @@ package com.margelo.nitro.autosizeinput
|
|
|
2
2
|
|
|
3
3
|
import android.graphics.Color
|
|
4
4
|
import android.graphics.Paint
|
|
5
|
+
import android.graphics.Rect
|
|
5
6
|
import android.graphics.Typeface
|
|
7
|
+
import android.graphics.drawable.GradientDrawable
|
|
6
8
|
import android.text.Editable
|
|
7
9
|
import android.text.InputType
|
|
8
10
|
import android.view.inputmethod.EditorInfo
|
|
@@ -116,7 +118,7 @@ class HybridAutoSizeInput(val context: ThemedReactContext) : HybridAutoSizeInput
|
|
|
116
118
|
recalculateFontSize()
|
|
117
119
|
}
|
|
118
120
|
|
|
119
|
-
override var multiline: Boolean?
|
|
121
|
+
override var multiline: Boolean? = null
|
|
120
122
|
get() = field
|
|
121
123
|
set(value) {
|
|
122
124
|
if (isDisposed) return
|
|
@@ -124,7 +126,7 @@ class HybridAutoSizeInput(val context: ThemedReactContext) : HybridAutoSizeInput
|
|
|
124
126
|
updateInputMode()
|
|
125
127
|
}
|
|
126
128
|
|
|
127
|
-
override var maxNumberOfLines: Double?
|
|
129
|
+
override var maxNumberOfLines: Double? = null
|
|
128
130
|
get() = field
|
|
129
131
|
set(value) {
|
|
130
132
|
if (isDisposed) return
|
|
@@ -132,7 +134,7 @@ class HybridAutoSizeInput(val context: ThemedReactContext) : HybridAutoSizeInput
|
|
|
132
134
|
recalculateFontSize()
|
|
133
135
|
}
|
|
134
136
|
|
|
135
|
-
override var textColor: String?
|
|
137
|
+
override var textColor: String? = null
|
|
136
138
|
get() = field
|
|
137
139
|
set(value) {
|
|
138
140
|
if (isDisposed) return
|
|
@@ -143,7 +145,7 @@ class HybridAutoSizeInput(val context: ThemedReactContext) : HybridAutoSizeInput
|
|
|
143
145
|
if (suffixColor == null) suffixView.setTextColor(color)
|
|
144
146
|
}
|
|
145
147
|
|
|
146
|
-
override var prefixColor: String?
|
|
148
|
+
override var prefixColor: String? = null
|
|
147
149
|
get() = field
|
|
148
150
|
set(value) {
|
|
149
151
|
if (isDisposed) return
|
|
@@ -151,7 +153,7 @@ class HybridAutoSizeInput(val context: ThemedReactContext) : HybridAutoSizeInput
|
|
|
151
153
|
prefixView.setTextColor(parseColor(value) ?: parseColor(textColor) ?: Color.BLACK)
|
|
152
154
|
}
|
|
153
155
|
|
|
154
|
-
override var suffixColor: String?
|
|
156
|
+
override var suffixColor: String? = null
|
|
155
157
|
get() = field
|
|
156
158
|
set(value) {
|
|
157
159
|
if (isDisposed) return
|
|
@@ -159,7 +161,7 @@ class HybridAutoSizeInput(val context: ThemedReactContext) : HybridAutoSizeInput
|
|
|
159
161
|
suffixView.setTextColor(parseColor(value) ?: parseColor(textColor) ?: Color.BLACK)
|
|
160
162
|
}
|
|
161
163
|
|
|
162
|
-
override var placeholderColor: String?
|
|
164
|
+
override var placeholderColor: String? = null
|
|
163
165
|
get() = field
|
|
164
166
|
set(value) {
|
|
165
167
|
if (isDisposed) return
|
|
@@ -167,7 +169,7 @@ class HybridAutoSizeInput(val context: ThemedReactContext) : HybridAutoSizeInput
|
|
|
167
169
|
inputView.setHintTextColor(parseColor(value) ?: Color.GRAY)
|
|
168
170
|
}
|
|
169
171
|
|
|
170
|
-
override var textAlign: String?
|
|
172
|
+
override var textAlign: String? = null
|
|
171
173
|
get() = field
|
|
172
174
|
set(value) {
|
|
173
175
|
if (isDisposed) return
|
|
@@ -180,7 +182,7 @@ class HybridAutoSizeInput(val context: ThemedReactContext) : HybridAutoSizeInput
|
|
|
180
182
|
inputView.gravity = gravity
|
|
181
183
|
}
|
|
182
184
|
|
|
183
|
-
override var fontFamily: String?
|
|
185
|
+
override var fontFamily: String? = null
|
|
184
186
|
get() = field
|
|
185
187
|
set(value) {
|
|
186
188
|
if (isDisposed) return
|
|
@@ -188,7 +190,7 @@ class HybridAutoSizeInput(val context: ThemedReactContext) : HybridAutoSizeInput
|
|
|
188
190
|
recalculateFontSize()
|
|
189
191
|
}
|
|
190
192
|
|
|
191
|
-
override var fontWeight: String?
|
|
193
|
+
override var fontWeight: String? = null
|
|
192
194
|
get() = field
|
|
193
195
|
set(value) {
|
|
194
196
|
if (isDisposed) return
|
|
@@ -196,7 +198,7 @@ class HybridAutoSizeInput(val context: ThemedReactContext) : HybridAutoSizeInput
|
|
|
196
198
|
recalculateFontSize()
|
|
197
199
|
}
|
|
198
200
|
|
|
199
|
-
override var editable: Boolean?
|
|
201
|
+
override var editable: Boolean? = null
|
|
200
202
|
get() = field
|
|
201
203
|
set(value) {
|
|
202
204
|
if (isDisposed) return
|
|
@@ -207,7 +209,7 @@ class HybridAutoSizeInput(val context: ThemedReactContext) : HybridAutoSizeInput
|
|
|
207
209
|
inputView.isCursorVisible = isEditable
|
|
208
210
|
}
|
|
209
211
|
|
|
210
|
-
override var keyboardType: String?
|
|
212
|
+
override var keyboardType: String? = null
|
|
211
213
|
get() = field
|
|
212
214
|
set(value) {
|
|
213
215
|
if (isDisposed) return
|
|
@@ -221,7 +223,7 @@ class HybridAutoSizeInput(val context: ThemedReactContext) : HybridAutoSizeInput
|
|
|
221
223
|
}
|
|
222
224
|
}
|
|
223
225
|
|
|
224
|
-
override var returnKeyType: String?
|
|
226
|
+
override var returnKeyType: String? = null
|
|
225
227
|
get() = field
|
|
226
228
|
set(value) {
|
|
227
229
|
if (isDisposed) return
|
|
@@ -236,7 +238,7 @@ class HybridAutoSizeInput(val context: ThemedReactContext) : HybridAutoSizeInput
|
|
|
236
238
|
}
|
|
237
239
|
}
|
|
238
240
|
|
|
239
|
-
override var autoCorrect: Boolean?
|
|
241
|
+
override var autoCorrect: Boolean? = null
|
|
240
242
|
get() = field
|
|
241
243
|
set(value) {
|
|
242
244
|
if (isDisposed) return
|
|
@@ -249,7 +251,7 @@ class HybridAutoSizeInput(val context: ThemedReactContext) : HybridAutoSizeInput
|
|
|
249
251
|
}
|
|
250
252
|
}
|
|
251
253
|
|
|
252
|
-
override var autoCapitalize: String?
|
|
254
|
+
override var autoCapitalize: String? = null
|
|
253
255
|
get() = field
|
|
254
256
|
set(value) {
|
|
255
257
|
if (isDisposed) return
|
|
@@ -269,7 +271,7 @@ class HybridAutoSizeInput(val context: ThemedReactContext) : HybridAutoSizeInput
|
|
|
269
271
|
inputView.inputType = cleared or capFlag
|
|
270
272
|
}
|
|
271
273
|
|
|
272
|
-
override var selectionColor: String?
|
|
274
|
+
override var selectionColor: String? = null
|
|
273
275
|
get() = field
|
|
274
276
|
set(value) {
|
|
275
277
|
if (isDisposed) return
|
|
@@ -286,7 +288,7 @@ class HybridAutoSizeInput(val context: ThemedReactContext) : HybridAutoSizeInput
|
|
|
286
288
|
}
|
|
287
289
|
}
|
|
288
290
|
|
|
289
|
-
override var prefixMarginRight: Double?
|
|
291
|
+
override var prefixMarginRight: Double? = null
|
|
290
292
|
get() = field
|
|
291
293
|
set(value) {
|
|
292
294
|
if (isDisposed) return
|
|
@@ -294,7 +296,7 @@ class HybridAutoSizeInput(val context: ThemedReactContext) : HybridAutoSizeInput
|
|
|
294
296
|
view.requestLayout()
|
|
295
297
|
}
|
|
296
298
|
|
|
297
|
-
override var suffixMarginLeft: Double?
|
|
299
|
+
override var suffixMarginLeft: Double? = null
|
|
298
300
|
get() = field
|
|
299
301
|
set(value) {
|
|
300
302
|
if (isDisposed) return
|
|
@@ -302,6 +304,31 @@ class HybridAutoSizeInput(val context: ThemedReactContext) : HybridAutoSizeInput
|
|
|
302
304
|
view.requestLayout()
|
|
303
305
|
}
|
|
304
306
|
|
|
307
|
+
override var showBorder: Boolean? = null
|
|
308
|
+
get() = field
|
|
309
|
+
set(value) {
|
|
310
|
+
if (isDisposed) return
|
|
311
|
+
field = value
|
|
312
|
+
updateInputAppearance()
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
override var inputBackgroundColor: String? = null
|
|
316
|
+
get() = field
|
|
317
|
+
set(value) {
|
|
318
|
+
if (isDisposed) return
|
|
319
|
+
field = value
|
|
320
|
+
updateInputAppearance()
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
override var contentAutoWidth: Boolean? = null
|
|
324
|
+
get() = field
|
|
325
|
+
set(value) {
|
|
326
|
+
if (isDisposed) return
|
|
327
|
+
field = value
|
|
328
|
+
view.requestLayout()
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
|
|
305
332
|
override var onChangeText: ((String) -> Unit)? = null
|
|
306
333
|
override var onFocus: (() -> Unit)? = null
|
|
307
334
|
override var onBlur: (() -> Unit)? = null
|
|
@@ -314,10 +341,18 @@ class HybridAutoSizeInput(val context: ThemedReactContext) : HybridAutoSizeInput
|
|
|
314
341
|
// Configure prefix
|
|
315
342
|
prefixView.visibility = View.GONE
|
|
316
343
|
prefixView.includeFontPadding = false
|
|
344
|
+
prefixView.isSingleLine = true
|
|
345
|
+
prefixView.maxLines = 1
|
|
346
|
+
prefixView.setHorizontallyScrolling(true)
|
|
347
|
+
prefixView.gravity = Gravity.CENTER_VERTICAL
|
|
317
348
|
|
|
318
349
|
// Configure suffix
|
|
319
350
|
suffixView.visibility = View.GONE
|
|
320
351
|
suffixView.includeFontPadding = false
|
|
352
|
+
suffixView.isSingleLine = true
|
|
353
|
+
suffixView.maxLines = 1
|
|
354
|
+
suffixView.setHorizontallyScrolling(true)
|
|
355
|
+
suffixView.gravity = Gravity.CENTER_VERTICAL
|
|
321
356
|
|
|
322
357
|
// Configure input
|
|
323
358
|
inputView.background = null
|
|
@@ -325,6 +360,10 @@ class HybridAutoSizeInput(val context: ThemedReactContext) : HybridAutoSizeInput
|
|
|
325
360
|
inputView.includeFontPadding = false
|
|
326
361
|
inputView.isSingleLine = true
|
|
327
362
|
inputView.maxLines = 1
|
|
363
|
+
inputView.setHorizontallyScrolling(true)
|
|
364
|
+
inputView.isVerticalScrollBarEnabled = false
|
|
365
|
+
inputView.overScrollMode = View.OVER_SCROLL_NEVER
|
|
366
|
+
inputView.gravity = Gravity.CENTER_VERTICAL or Gravity.START
|
|
328
367
|
inputView.addTextChangedListener(textWatcher)
|
|
329
368
|
|
|
330
369
|
inputView.setOnFocusChangeListener { _, hasFocus ->
|
|
@@ -332,10 +371,22 @@ class HybridAutoSizeInput(val context: ThemedReactContext) : HybridAutoSizeInput
|
|
|
332
371
|
if (hasFocus) onFocus?.invoke() else onBlur?.invoke()
|
|
333
372
|
}
|
|
334
373
|
|
|
374
|
+
// Make the whole composed area (prefix + input + suffix) tappable for focus.
|
|
375
|
+
val focusFromContainer = View.OnClickListener {
|
|
376
|
+
if (isDisposed || editable == false) return@OnClickListener
|
|
377
|
+
requestInputFocus()
|
|
378
|
+
}
|
|
379
|
+
view.isClickable = true
|
|
380
|
+
view.setOnClickListener(focusFromContainer)
|
|
381
|
+
prefixView.setOnClickListener(focusFromContainer)
|
|
382
|
+
inputView.setOnClickListener(focusFromContainer)
|
|
383
|
+
suffixView.setOnClickListener(focusFromContainer)
|
|
384
|
+
|
|
335
385
|
// Add to container
|
|
336
386
|
(view as ViewGroup).addView(prefixView)
|
|
337
387
|
(view as ViewGroup).addView(inputView)
|
|
338
388
|
(view as ViewGroup).addView(suffixView)
|
|
389
|
+
updateInputAppearance()
|
|
339
390
|
}
|
|
340
391
|
|
|
341
392
|
private fun updateInputMode() {
|
|
@@ -356,43 +407,78 @@ class HybridAutoSizeInput(val context: ThemedReactContext) : HybridAutoSizeInput
|
|
|
356
407
|
if (width <= 0 || height <= 0) return
|
|
357
408
|
|
|
358
409
|
val density = context.resources.displayMetrics.density
|
|
410
|
+
val edgeInset = (2f * density).toInt()
|
|
359
411
|
|
|
360
412
|
// Measure prefix
|
|
361
413
|
val prefixW = if (prefixView.visibility == View.VISIBLE) {
|
|
362
|
-
prefixView
|
|
363
|
-
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.AT_MOST),
|
|
364
|
-
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)
|
|
365
|
-
)
|
|
366
|
-
prefixView.measuredWidth
|
|
414
|
+
measureTextViewWidthPx(prefixView)
|
|
367
415
|
} else 0
|
|
368
416
|
|
|
369
417
|
// Measure suffix
|
|
370
418
|
val suffixW = if (suffixView.visibility == View.VISIBLE) {
|
|
371
|
-
suffixView
|
|
372
|
-
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.AT_MOST),
|
|
373
|
-
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)
|
|
374
|
-
)
|
|
375
|
-
suffixView.measuredWidth
|
|
419
|
+
measureTextViewWidthPx(suffixView)
|
|
376
420
|
} else 0
|
|
377
421
|
|
|
378
422
|
val prefixGap = if (prefixView.visibility == View.VISIBLE) ((prefixMarginRight ?: 0.0) * density).toInt() else 0
|
|
379
423
|
val suffixGap = if (suffixView.visibility == View.VISIBLE) ((suffixMarginLeft ?: 0.0) * density).toInt() else 0
|
|
380
424
|
|
|
381
|
-
val inputX = prefixW + prefixGap
|
|
382
|
-
val
|
|
425
|
+
val inputX = edgeInset + prefixW + prefixGap
|
|
426
|
+
val isContentAutoWidthEnabled = contentAutoWidth == true && multiline != true
|
|
427
|
+
val inputW: Int
|
|
428
|
+
val suffixX: Int
|
|
429
|
+
|
|
430
|
+
if (isContentAutoWidthEnabled) {
|
|
431
|
+
val typedText = inputView.text?.toString() ?: ""
|
|
432
|
+
val minInputWidth = (24f * density).toInt()
|
|
433
|
+
val desiredInputWidth = maxOf(measureSingleLineTextWidthPx(typedText), minInputWidth)
|
|
434
|
+
val suffixSegment = if (suffixView.visibility == View.VISIBLE) suffixGap + suffixW else 0
|
|
435
|
+
val maxInputWidth = maxOf(width - edgeInset - inputX - suffixSegment, 0)
|
|
436
|
+
inputW = minOf(desiredInputWidth, maxInputWidth)
|
|
437
|
+
val desiredSuffixX = if (suffixView.visibility == View.VISIBLE) inputX + inputW + suffixGap else width - edgeInset
|
|
438
|
+
suffixX = minOf(desiredSuffixX, width - edgeInset - suffixW)
|
|
439
|
+
} else {
|
|
440
|
+
inputW = maxOf(width - edgeInset - inputX - suffixW - suffixGap, 0)
|
|
441
|
+
suffixX = width - edgeInset - suffixW
|
|
442
|
+
}
|
|
383
443
|
|
|
384
|
-
//
|
|
385
|
-
prefixView.
|
|
444
|
+
// Re-measure with the exact final slot size before layout.
|
|
445
|
+
if (prefixView.visibility == View.VISIBLE) {
|
|
446
|
+
prefixView.measure(
|
|
447
|
+
View.MeasureSpec.makeMeasureSpec(prefixW, View.MeasureSpec.EXACTLY),
|
|
448
|
+
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.AT_MOST)
|
|
449
|
+
)
|
|
450
|
+
}
|
|
451
|
+
if (suffixView.visibility == View.VISIBLE) {
|
|
452
|
+
suffixView.measure(
|
|
453
|
+
View.MeasureSpec.makeMeasureSpec(suffixW, View.MeasureSpec.EXACTLY),
|
|
454
|
+
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.AT_MOST)
|
|
455
|
+
)
|
|
456
|
+
}
|
|
386
457
|
|
|
387
458
|
// Layout input
|
|
459
|
+
val inputHeightSpec = if (multiline == true) {
|
|
460
|
+
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)
|
|
461
|
+
} else {
|
|
462
|
+
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.AT_MOST)
|
|
463
|
+
}
|
|
388
464
|
inputView.measure(
|
|
389
465
|
View.MeasureSpec.makeMeasureSpec(inputW, View.MeasureSpec.EXACTLY),
|
|
390
|
-
|
|
466
|
+
inputHeightSpec
|
|
391
467
|
)
|
|
392
|
-
|
|
468
|
+
val inputH = if (multiline == true) height else inputView.measuredHeight.coerceAtMost(height)
|
|
469
|
+
val inputTop = if (multiline == true) 0 else ((height - inputH) / 2).coerceAtLeast(0)
|
|
470
|
+
inputView.layout(inputX, inputTop, inputX + inputW, inputTop + inputH)
|
|
471
|
+
resetSingleLineVerticalOffset()
|
|
472
|
+
|
|
473
|
+
val prefixTop = ((height - prefixView.measuredHeight) / 2).coerceAtLeast(0)
|
|
474
|
+
val suffixTop = ((height - suffixView.measuredHeight) / 2).coerceAtLeast(0)
|
|
475
|
+
|
|
476
|
+
// Layout prefix
|
|
477
|
+
prefixView.layout(edgeInset, prefixTop, edgeInset + prefixW, prefixTop + prefixView.measuredHeight)
|
|
393
478
|
|
|
394
479
|
// Layout suffix
|
|
395
|
-
suffixView.layout(
|
|
480
|
+
suffixView.layout(suffixX, suffixTop, suffixX + suffixW, suffixTop + suffixView.measuredHeight)
|
|
481
|
+
|
|
396
482
|
}
|
|
397
483
|
|
|
398
484
|
// MARK: - Font Size Calculation
|
|
@@ -400,42 +486,76 @@ class HybridAutoSizeInput(val context: ThemedReactContext) : HybridAutoSizeInput
|
|
|
400
486
|
private fun recalculateFontSize() {
|
|
401
487
|
if (isDisposed || isRecalculating) return
|
|
402
488
|
isRecalculating = true
|
|
489
|
+
try {
|
|
490
|
+
val maxSize = (maxFontSizeProp ?: 48.0).toFloat()
|
|
491
|
+
val minSize = (minFontSizeProp ?: 16.0).toFloat()
|
|
492
|
+
val isContentAutoWidthEnabled = contentAutoWidth == true && multiline != true
|
|
493
|
+
val width = view.width
|
|
494
|
+
val height = view.height
|
|
495
|
+
val inputText = inputView.text?.toString() ?: ""
|
|
496
|
+
if (width <= 0 || height <= 0) {
|
|
497
|
+
// Keep text size in sync even before first valid layout pass.
|
|
498
|
+
applyFontSize(maxSize)
|
|
499
|
+
return
|
|
500
|
+
}
|
|
403
501
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
502
|
+
// In contentAutoWidth mode, prioritize width expansion instead of shrinking text.
|
|
503
|
+
if (isContentAutoWidthEnabled) {
|
|
504
|
+
val density = context.resources.displayMetrics.density
|
|
505
|
+
val edgeInset = (2f * density).toInt()
|
|
506
|
+
val prefixW = if (prefixView.visibility == View.VISIBLE) measureTextViewWidthPx(prefixView) else 0
|
|
507
|
+
val suffixW = if (suffixView.visibility == View.VISIBLE) measureTextViewWidthPx(suffixView) else 0
|
|
508
|
+
val prefixGap = if (prefixView.visibility == View.VISIBLE) ((prefixMarginRight ?: 0.0) * density).toInt() else 0
|
|
509
|
+
val suffixGap = if (suffixView.visibility == View.VISIBLE) ((suffixMarginLeft ?: 0.0) * density).toInt() else 0
|
|
510
|
+
val inputX = edgeInset + prefixW + prefixGap
|
|
511
|
+
val suffixSegment = if (suffixView.visibility == View.VISIBLE) suffixGap + suffixW else 0
|
|
512
|
+
val maxInputWidth = maxOf(width - edgeInset - inputX - suffixSegment, 0)
|
|
513
|
+
val textForSizing = if (inputText.isEmpty()) (placeholder ?: "") else inputText
|
|
514
|
+
|
|
515
|
+
// Expand width first; once width hits max, shrink font to keep full text visible.
|
|
516
|
+
val targetSize = if (maxInputWidth <= 0) {
|
|
517
|
+
minSize
|
|
518
|
+
} else if (textForSizing.isEmpty()) {
|
|
519
|
+
maxSize
|
|
520
|
+
} else {
|
|
521
|
+
findOptimalFontSizeSingleLine(
|
|
522
|
+
fullText = textForSizing,
|
|
523
|
+
availableWidth = maxInputWidth.toFloat(),
|
|
524
|
+
minSize = minSize,
|
|
525
|
+
maxSize = maxSize
|
|
526
|
+
)
|
|
527
|
+
}
|
|
528
|
+
applyFontSize(targetSize)
|
|
529
|
+
return
|
|
530
|
+
}
|
|
410
531
|
|
|
411
|
-
|
|
412
|
-
val maxSize = (maxFontSizeProp ?: 48.0).toFloat()
|
|
413
|
-
val minSize = (minFontSizeProp ?: 16.0).toFloat()
|
|
532
|
+
val density = context.resources.displayMetrics.density
|
|
414
533
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
val fullText = prefixText + displayText + suffixText
|
|
534
|
+
val prefixText = prefix ?: ""
|
|
535
|
+
val suffixText = suffix ?: ""
|
|
536
|
+
val displayText = if (inputText.isEmpty()) (placeholder ?: "") else inputText
|
|
537
|
+
val fullText = prefixText + displayText + suffixText
|
|
420
538
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
539
|
+
if (fullText.isEmpty()) {
|
|
540
|
+
applyFontSize(maxSize)
|
|
541
|
+
return
|
|
542
|
+
}
|
|
425
543
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
544
|
+
val prefixGap = if (prefixView.visibility == View.VISIBLE) ((prefixMarginRight ?: 0.0) * density) else 0.0
|
|
545
|
+
val suffixGap = if (suffixView.visibility == View.VISIBLE) ((suffixMarginLeft ?: 0.0) * density) else 0.0
|
|
546
|
+
val availableWidth = width - prefixGap.toFloat() - suffixGap.toFloat()
|
|
429
547
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
548
|
+
val optimalSize = if (multiline == true) {
|
|
549
|
+
val maxLines = (maxNumberOfLines ?: 1.0).toInt()
|
|
550
|
+
findOptimalFontSizeMultiline(fullText, availableWidth, height.toFloat(), maxLines, minSize, maxSize)
|
|
551
|
+
} else {
|
|
552
|
+
findOptimalFontSizeSingleLine(fullText, availableWidth, minSize, maxSize)
|
|
553
|
+
}
|
|
436
554
|
|
|
437
|
-
|
|
438
|
-
|
|
555
|
+
applyFontSize(optimalSize)
|
|
556
|
+
} finally {
|
|
557
|
+
isRecalculating = false
|
|
558
|
+
}
|
|
439
559
|
}
|
|
440
560
|
|
|
441
561
|
private fun findOptimalFontSizeSingleLine(
|
|
@@ -503,7 +623,12 @@ class HybridAutoSizeInput(val context: ThemedReactContext) : HybridAutoSizeInput
|
|
|
503
623
|
prefixView.typeface = typeface
|
|
504
624
|
suffixView.setTextSize(TypedValue.COMPLEX_UNIT_SP, size)
|
|
505
625
|
suffixView.typeface = typeface
|
|
506
|
-
|
|
626
|
+
resetSingleLineVerticalOffset()
|
|
627
|
+
if (contentAutoWidth == true && multiline != true && view.width > 0 && view.height > 0) {
|
|
628
|
+
performLayout(view.width, view.height)
|
|
629
|
+
} else {
|
|
630
|
+
view.requestLayout()
|
|
631
|
+
}
|
|
507
632
|
}
|
|
508
633
|
|
|
509
634
|
private fun makeTypeface(): Typeface {
|
|
@@ -527,9 +652,7 @@ class HybridAutoSizeInput(val context: ThemedReactContext) : HybridAutoSizeInput
|
|
|
527
652
|
|
|
528
653
|
override fun focus() {
|
|
529
654
|
if (isDisposed) return
|
|
530
|
-
|
|
531
|
-
val imm = context.getSystemService(android.content.Context.INPUT_METHOD_SERVICE) as? android.view.inputmethod.InputMethodManager
|
|
532
|
-
imm?.showSoftInput(inputView, android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT)
|
|
655
|
+
requestInputFocus()
|
|
533
656
|
}
|
|
534
657
|
|
|
535
658
|
override fun blur() {
|
|
@@ -550,6 +673,58 @@ class HybridAutoSizeInput(val context: ThemedReactContext) : HybridAutoSizeInput
|
|
|
550
673
|
}
|
|
551
674
|
}
|
|
552
675
|
|
|
676
|
+
private fun updateInputAppearance() {
|
|
677
|
+
val drawable = GradientDrawable()
|
|
678
|
+
drawable.setColor(parseColor(inputBackgroundColor) ?: Color.TRANSPARENT)
|
|
679
|
+
if (showBorder == true) {
|
|
680
|
+
val borderWidthPx = context.resources.displayMetrics.density.toInt().coerceAtLeast(1)
|
|
681
|
+
drawable.setStroke(borderWidthPx, parseColor(textColor) ?: Color.parseColor("#D1D5DB"))
|
|
682
|
+
} else {
|
|
683
|
+
drawable.setStroke(0, Color.TRANSPARENT)
|
|
684
|
+
}
|
|
685
|
+
inputView.background = drawable
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
private fun measureSingleLineTextWidthPx(text: String): Int {
|
|
689
|
+
if (text.isEmpty()) return 0
|
|
690
|
+
val paint = TextPaint(Paint.ANTI_ALIAS_FLAG)
|
|
691
|
+
paint.textSize = currentFontSize * context.resources.displayMetrics.scaledDensity
|
|
692
|
+
paint.typeface = makeTypeface()
|
|
693
|
+
return kotlin.math.ceil(paint.measureText(text).toDouble()).toInt()
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
private fun measureTextViewWidthPx(textView: TextView): Int {
|
|
697
|
+
val content = textView.text?.toString().orEmpty()
|
|
698
|
+
if (content.isEmpty()) return 0
|
|
699
|
+
val bounds = Rect()
|
|
700
|
+
textView.paint.getTextBounds(content, 0, content.length, bounds)
|
|
701
|
+
val advanceWidth = kotlin.math.ceil(textView.paint.measureText(content).toDouble()).toInt()
|
|
702
|
+
val glyphWidth = bounds.width()
|
|
703
|
+
textView.measure(
|
|
704
|
+
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
|
|
705
|
+
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
|
|
706
|
+
)
|
|
707
|
+
val desiredWidth = textView.measuredWidth
|
|
708
|
+
val density = context.resources.displayMetrics.density
|
|
709
|
+
// Keep extra room for side bearings/kerning to avoid clipping on some glyphs/fonts.
|
|
710
|
+
val safetyPadding = maxOf((4f * density).toInt(), kotlin.math.ceil(textView.textSize * 0.5f).toInt())
|
|
711
|
+
val measured = maxOf(maxOf(advanceWidth, glyphWidth), desiredWidth) + safetyPadding
|
|
712
|
+
return measured
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
private fun requestInputFocus() {
|
|
716
|
+
inputView.requestFocus()
|
|
717
|
+
val imm = context.getSystemService(android.content.Context.INPUT_METHOD_SERVICE) as? android.view.inputmethod.InputMethodManager
|
|
718
|
+
imm?.showSoftInput(inputView, android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT)
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
private fun resetSingleLineVerticalOffset() {
|
|
722
|
+
if (multiline == true) return
|
|
723
|
+
if (inputView.scrollY != 0) {
|
|
724
|
+
inputView.scrollTo(inputView.scrollX, 0)
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
553
728
|
override fun afterUpdate() {
|
|
554
729
|
super.afterUpdate()
|
|
555
730
|
if (!isDisposed) {
|
package/ios/AutoSizeInput.swift
CHANGED
|
@@ -14,6 +14,7 @@ class HybridAutoSizeInput: HybridAutoSizeInputSpec {
|
|
|
14
14
|
private var isRecalculating = false
|
|
15
15
|
private var currentFontSize: CGFloat = 48
|
|
16
16
|
private var inputDelegate: InputDelegate?
|
|
17
|
+
private var containerTapGesture: UITapGestureRecognizer?
|
|
17
18
|
|
|
18
19
|
// MARK: - HybridView
|
|
19
20
|
var view: UIView = UIView()
|
|
@@ -29,6 +30,7 @@ class HybridAutoSizeInput: HybridAutoSizeInputSpec {
|
|
|
29
30
|
singleLineInput.text = text ?? ""
|
|
30
31
|
}
|
|
31
32
|
isUpdatingFromJS = false
|
|
33
|
+
view.setNeedsLayout()
|
|
32
34
|
recalculateFontSize()
|
|
33
35
|
}
|
|
34
36
|
}
|
|
@@ -190,6 +192,24 @@ class HybridAutoSizeInput: HybridAutoSizeInputSpec {
|
|
|
190
192
|
}
|
|
191
193
|
}
|
|
192
194
|
|
|
195
|
+
var showBorder: Bool? {
|
|
196
|
+
didSet {
|
|
197
|
+
updateInputAppearance()
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
var inputBackgroundColor: String? {
|
|
202
|
+
didSet {
|
|
203
|
+
updateInputAppearance()
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
var contentAutoWidth: Bool? {
|
|
208
|
+
didSet {
|
|
209
|
+
view.setNeedsLayout()
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
193
213
|
var onChangeText: ((String) -> Void)?
|
|
194
214
|
var onFocus: (() -> Void)?
|
|
195
215
|
var onBlur: (() -> Void)?
|
|
@@ -227,7 +247,6 @@ class HybridAutoSizeInput: HybridAutoSizeInputSpec {
|
|
|
227
247
|
multiLineInput.delegate = delegate
|
|
228
248
|
multiLineInput.textContainerInset = .zero
|
|
229
249
|
multiLineInput.textContainer.lineFragmentPadding = 0
|
|
230
|
-
multiLineInput.backgroundColor = .clear
|
|
231
250
|
multiLineInput.isScrollEnabled = false
|
|
232
251
|
multiLineInput.isHidden = true
|
|
233
252
|
|
|
@@ -249,6 +268,13 @@ class HybridAutoSizeInput: HybridAutoSizeInputSpec {
|
|
|
249
268
|
subview.removeFromSuperview()
|
|
250
269
|
view.addSubview(subview)
|
|
251
270
|
}
|
|
271
|
+
|
|
272
|
+
// Keep input usable when contentAutoWidth starts from 0 width.
|
|
273
|
+
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleContainerTap))
|
|
274
|
+
tapGesture.cancelsTouchesInView = false
|
|
275
|
+
view.addGestureRecognizer(tapGesture)
|
|
276
|
+
containerTapGesture = tapGesture
|
|
277
|
+
updateInputAppearance()
|
|
252
278
|
}
|
|
253
279
|
|
|
254
280
|
private func performLayout() {
|
|
@@ -277,13 +303,28 @@ class HybridAutoSizeInput: HybridAutoSizeInputSpec {
|
|
|
277
303
|
let suffixGap = suffixLabel.isHidden ? 0 : CGFloat(suffixMarginLeft ?? 0)
|
|
278
304
|
|
|
279
305
|
let inputX = prefixW + prefixGap
|
|
280
|
-
let
|
|
306
|
+
let isContentAutoWidthEnabled = contentAutoWidth == true && multiline != true
|
|
307
|
+
let inputW: CGFloat
|
|
308
|
+
let suffixX: CGFloat
|
|
309
|
+
|
|
310
|
+
if isContentAutoWidthEnabled {
|
|
311
|
+
let typedText = singleLineInput.text ?? ""
|
|
312
|
+
let desiredInputWidth = measuredSingleLineTextWidth(typedText, font: singleLineInput.font)
|
|
313
|
+
let suffixSegment = suffixLabel.isHidden ? 0 : (suffixGap + suffixW)
|
|
314
|
+
let maxInputWidth = max(bounds.width - inputX - suffixSegment, 0)
|
|
315
|
+
inputW = min(desiredInputWidth, maxInputWidth)
|
|
316
|
+
let desiredSuffixX = suffixLabel.isHidden ? bounds.width : (inputX + inputW + suffixGap)
|
|
317
|
+
suffixX = min(desiredSuffixX, bounds.width - suffixW)
|
|
318
|
+
} else {
|
|
319
|
+
inputW = max(bounds.width - inputX - suffixW - suffixGap, 0)
|
|
320
|
+
suffixX = bounds.width - suffixW
|
|
321
|
+
}
|
|
281
322
|
|
|
282
323
|
prefixLabel.frame = CGRect(x: 0, y: 0, width: prefixW, height: bounds.height)
|
|
283
|
-
suffixLabel.frame = CGRect(x:
|
|
324
|
+
suffixLabel.frame = CGRect(x: suffixX, y: 0, width: suffixW, height: bounds.height)
|
|
284
325
|
|
|
285
326
|
let activeInput: UIView = (multiline == true) ? multiLineInput : singleLineInput
|
|
286
|
-
activeInput.frame = CGRect(x: inputX, y: 0, width:
|
|
327
|
+
activeInput.frame = CGRect(x: inputX, y: 0, width: inputW, height: bounds.height)
|
|
287
328
|
}
|
|
288
329
|
|
|
289
330
|
private func updateInputMode() {
|
|
@@ -472,12 +513,14 @@ class HybridAutoSizeInput: HybridAutoSizeInputSpec {
|
|
|
472
513
|
|
|
473
514
|
fileprivate func handleTextFieldChange(_ textField: UITextField) {
|
|
474
515
|
guard !isUpdatingFromJS else { return }
|
|
516
|
+
view.setNeedsLayout()
|
|
475
517
|
recalculateFontSize()
|
|
476
518
|
onChangeText?(textField.text ?? "")
|
|
477
519
|
}
|
|
478
520
|
|
|
479
521
|
fileprivate func handleTextViewChange(_ textView: UITextView) {
|
|
480
522
|
guard !isUpdatingFromJS else { return }
|
|
523
|
+
view.setNeedsLayout()
|
|
481
524
|
recalculateFontSize()
|
|
482
525
|
onChangeText?(textView.text ?? "")
|
|
483
526
|
}
|
|
@@ -490,6 +533,15 @@ class HybridAutoSizeInput: HybridAutoSizeInputSpec {
|
|
|
490
533
|
onBlur?()
|
|
491
534
|
}
|
|
492
535
|
|
|
536
|
+
@objc private func handleContainerTap() {
|
|
537
|
+
guard editable != false else { return }
|
|
538
|
+
if multiline == true {
|
|
539
|
+
_ = multiLineInput.becomeFirstResponder()
|
|
540
|
+
} else {
|
|
541
|
+
_ = singleLineInput.becomeFirstResponder()
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
493
545
|
// MARK: - Helpers
|
|
494
546
|
|
|
495
547
|
private func colorFromHex(_ hex: String?) -> UIColor? {
|
|
@@ -518,6 +570,25 @@ class HybridAutoSizeInput: HybridAutoSizeInputSpec {
|
|
|
518
570
|
}
|
|
519
571
|
}
|
|
520
572
|
|
|
573
|
+
private func updateInputAppearance() {
|
|
574
|
+
let borderWidth: CGFloat = (showBorder == true) ? 1 : 0
|
|
575
|
+
let backgroundColor = colorFromHex(inputBackgroundColor) ?? UIColor.clear
|
|
576
|
+
|
|
577
|
+
singleLineInput.layer.borderWidth = borderWidth
|
|
578
|
+
singleLineInput.layer.borderColor = UIColor.separator.cgColor
|
|
579
|
+
singleLineInput.backgroundColor = backgroundColor
|
|
580
|
+
|
|
581
|
+
multiLineInput.layer.borderWidth = borderWidth
|
|
582
|
+
multiLineInput.layer.borderColor = UIColor.separator.cgColor
|
|
583
|
+
multiLineInput.backgroundColor = backgroundColor
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
private func measuredSingleLineTextWidth(_ text: String, font: UIFont?) -> CGFloat {
|
|
587
|
+
guard !text.isEmpty else { return 0 }
|
|
588
|
+
let effectiveFont = font ?? makeFont(size: currentFontSize)
|
|
589
|
+
return (text as NSString).size(withAttributes: [.font: effectiveFont]).width
|
|
590
|
+
}
|
|
591
|
+
|
|
521
592
|
private func textAlignmentFrom(_ align: String?) -> NSTextAlignment {
|
|
522
593
|
switch align {
|
|
523
594
|
case "center": return .center
|