@onekeyfe/react-native-auto-size-input 1.1.28 → 1.1.30

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.
Files changed (32) hide show
  1. package/android/src/main/java/com/margelo/nitro/autosizeinput/AutoSizeInput.kt +242 -67
  2. package/ios/AutoSizeInput.swift +75 -4
  3. package/lib/nitrogen/generated/android/c++/JHybridAutoSizeInputSpec.cpp +27 -0
  4. package/lib/nitrogen/generated/android/c++/JHybridAutoSizeInputSpec.hpp +6 -0
  5. package/lib/nitrogen/generated/android/c++/views/JHybridAutoSizeInputStateUpdater.cpp +12 -0
  6. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/autosizeinput/HybridAutoSizeInputSpec.kt +18 -0
  7. package/lib/nitrogen/generated/ios/c++/HybridAutoSizeInputSpecSwift.hpp +21 -0
  8. package/lib/nitrogen/generated/ios/c++/views/HybridAutoSizeInputComponent.mm +15 -0
  9. package/lib/nitrogen/generated/ios/swift/HybridAutoSizeInputSpec.swift +3 -0
  10. package/lib/nitrogen/generated/ios/swift/HybridAutoSizeInputSpec_cxx.swift +72 -0
  11. package/lib/nitrogen/generated/shared/c++/HybridAutoSizeInputSpec.cpp +6 -0
  12. package/lib/nitrogen/generated/shared/c++/HybridAutoSizeInputSpec.hpp +6 -0
  13. package/lib/nitrogen/generated/shared/c++/views/HybridAutoSizeInputComponent.cpp +36 -0
  14. package/lib/nitrogen/generated/shared/c++/views/HybridAutoSizeInputComponent.hpp +3 -0
  15. package/lib/nitrogen/generated/shared/json/AutoSizeInputConfig.json +3 -0
  16. package/lib/typescript/src/AutoSizeInput.nitro.d.ts +3 -0
  17. package/lib/typescript/src/AutoSizeInput.nitro.d.ts.map +1 -1
  18. package/nitrogen/generated/android/c++/JHybridAutoSizeInputSpec.cpp +27 -0
  19. package/nitrogen/generated/android/c++/JHybridAutoSizeInputSpec.hpp +6 -0
  20. package/nitrogen/generated/android/c++/views/JHybridAutoSizeInputStateUpdater.cpp +12 -0
  21. package/nitrogen/generated/android/kotlin/com/margelo/nitro/autosizeinput/HybridAutoSizeInputSpec.kt +18 -0
  22. package/nitrogen/generated/ios/c++/HybridAutoSizeInputSpecSwift.hpp +21 -0
  23. package/nitrogen/generated/ios/c++/views/HybridAutoSizeInputComponent.mm +15 -0
  24. package/nitrogen/generated/ios/swift/HybridAutoSizeInputSpec.swift +3 -0
  25. package/nitrogen/generated/ios/swift/HybridAutoSizeInputSpec_cxx.swift +72 -0
  26. package/nitrogen/generated/shared/c++/HybridAutoSizeInputSpec.cpp +6 -0
  27. package/nitrogen/generated/shared/c++/HybridAutoSizeInputSpec.hpp +6 -0
  28. package/nitrogen/generated/shared/c++/views/HybridAutoSizeInputComponent.cpp +36 -0
  29. package/nitrogen/generated/shared/c++/views/HybridAutoSizeInputComponent.hpp +3 -0
  30. package/nitrogen/generated/shared/json/AutoSizeInputConfig.json +3 -0
  31. package/package.json +1 -1
  32. 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.measure(
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.measure(
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 inputW = maxOf(width - inputX - suffixW - suffixGap, 0)
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
- // Layout prefix
385
- prefixView.layout(0, 0, prefixW, height)
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
- View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)
466
+ inputHeightSpec
391
467
  )
392
- inputView.layout(inputX, 0, inputX + inputW, height)
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(width - suffixW, 0, width, height)
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
- val width = view.width
405
- val height = view.height
406
- if (width <= 0 || height <= 0) {
407
- isRecalculating = false
408
- return
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
- val density = context.resources.displayMetrics.density
412
- val maxSize = (maxFontSizeProp ?: 48.0).toFloat()
413
- val minSize = (minFontSizeProp ?: 16.0).toFloat()
532
+ val density = context.resources.displayMetrics.density
414
533
 
415
- val prefixText = prefix ?: ""
416
- val suffixText = suffix ?: ""
417
- val inputText = inputView.text?.toString() ?: ""
418
- val displayText = if (inputText.isEmpty()) (placeholder ?: "") else inputText
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
- if (fullText.isEmpty()) {
422
- applyFontSize(maxSize)
423
- return
424
- }
539
+ if (fullText.isEmpty()) {
540
+ applyFontSize(maxSize)
541
+ return
542
+ }
425
543
 
426
- val prefixGap = if (prefixView.visibility == View.VISIBLE) ((prefixMarginRight ?: 0.0) * density) else 0.0
427
- val suffixGap = if (suffixView.visibility == View.VISIBLE) ((suffixMarginLeft ?: 0.0) * density) else 0.0
428
- val availableWidth = width - prefixGap.toFloat() - suffixGap.toFloat()
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
- val optimalSize = if (multiline == true) {
431
- val maxLines = (maxNumberOfLines ?: 1.0).toInt()
432
- findOptimalFontSizeMultiline(fullText, availableWidth, height.toFloat(), maxLines, minSize, maxSize)
433
- } else {
434
- findOptimalFontSizeSingleLine(fullText, availableWidth, minSize, maxSize)
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
- applyFontSize(optimalSize)
438
- isRecalculating = false
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
- view.requestLayout()
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
- inputView.requestFocus()
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) {
@@ -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 inputW = bounds.width - inputX - suffixW - suffixGap
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: bounds.width - suffixW, y: 0, width: suffixW, height: bounds.height)
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: max(inputW, 0), height: bounds.height)
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