@onekeyfe/react-native-auto-size-input 3.0.131 → 3.0.132

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.
@@ -337,6 +337,10 @@ class HybridAutoSizeInput(val context: ThemedReactContext) : HybridAutoSizeInput
337
337
  if (isDisposed) return
338
338
  field = value
339
339
  view.requestLayout()
340
+ // requestLayout() is a no-op under React Native (the parent
341
+ // ReactViewGroup swallows it), so a gap change that arrives after the
342
+ // prefix/suffix props has to re-run the fit, which lays the row out again.
343
+ recalculateFontSize()
340
344
  }
341
345
 
342
346
  override var suffixMarginLeft: Double? = null
@@ -345,6 +349,10 @@ class HybridAutoSizeInput(val context: ThemedReactContext) : HybridAutoSizeInput
345
349
  if (isDisposed) return
346
350
  field = value
347
351
  view.requestLayout()
352
+ // requestLayout() is a no-op under React Native (the parent
353
+ // ReactViewGroup swallows it), so a gap change that arrives after the
354
+ // prefix/suffix props has to re-run the fit, which lays the row out again.
355
+ recalculateFontSize()
348
356
  }
349
357
 
350
358
  override var showBorder: Boolean? = null
@@ -595,31 +603,43 @@ class HybridAutoSizeInput(val context: ThemedReactContext) : HybridAutoSizeInput
595
603
  // In contentAutoWidth mode, prioritize width expansion instead of shrinking text.
596
604
  if (isContentAutoWidthEnabled) {
597
605
  val density = context.resources.displayMetrics.density
606
+ val scaledDensity = context.resources.displayMetrics.scaledDensity
598
607
  val edgeInset = (2f * density).toInt()
599
608
  val singleLineWidthPadding = contentAutoWidthPaddingPx()
600
- val prefixW = if (prefixView.visibility == View.VISIBLE) measureTextViewWidthPx(prefixView) else 0
601
- val suffixW = if (suffixView.visibility == View.VISIBLE) measureTextViewWidthPx(suffixView) else 0
609
+ val minInputWidth = (24f * density).toInt()
610
+ // Measure prefix/suffix at every candidate size instead of
611
+ // once at the current size. applyFontSize scales them together with the
612
+ // input, so a fit that used their old (smaller) width left performLayout
613
+ // with a slot narrower than the text and the EditText scrolled the
614
+ // leading digits out of view (send-amount fiat toggle: "$" prefix appears
615
+ // while the token suffix disappears and the font grows).
616
+ val prefixText = if (prefixView.visibility == View.VISIBLE) prefixView.text?.toString().orEmpty() else ""
617
+ val suffixText = if (suffixView.visibility == View.VISIBLE) suffixView.text?.toString().orEmpty() else ""
602
618
  val prefixGap = if (prefixView.visibility == View.VISIBLE) ((prefixMarginRight ?: 0.0) * density).toInt() else 0
603
619
  val suffixGap = if (suffixView.visibility == View.VISIBLE) ((suffixMarginLeft ?: 0.0) * density).toInt() else 0
604
- val prefixSegment = prefixW + prefixGap
605
- val suffixSegment = if (suffixView.visibility == View.VISIBLE) suffixGap + suffixW else 0
606
620
  val availableTrackWidth = maxOf(width - (edgeInset * 2), 0)
607
- val maxInputWidth = maxOf(availableTrackWidth - prefixSegment - suffixSegment, 0)
608
- val maxTextWidth = maxOf(maxInputWidth - singleLineWidthPadding, 0)
621
+ val availableGroupWidth = maxOf(availableTrackWidth - prefixGap - suffixGap, 0)
609
622
  val maxTextHeight = maxOf(height - inputView.paddingTop - inputView.paddingBottom, 0)
610
623
  val textForSizing = if (inputText.isEmpty()) (placeholder ?: "") else inputText
611
624
  val probeText = if (textForSizing.isEmpty()) "0" else textForSizing
612
-
613
- // Keep both width and line-height within the input slot.
614
- val widthFitSize = if (maxTextWidth <= 0) {
615
- minSize
616
- } else {
617
- findOptimalFontSizeSingleLine(
618
- fullText = probeText,
619
- availableWidth = maxTextWidth.toFloat(),
620
- minSize = minSize,
621
- maxSize = maxSize
625
+ val typeface = makeTypeface()
626
+
627
+ // Keep both width and line-height within the input slot. The width probe
628
+ // mirrors performLayout: input slot (text + padding, at least the minimum
629
+ // width) plus both side labels, all at the candidate size.
630
+ val widthFitSize = AutoWidthFontFit.findLargestFittingSize(
631
+ minSize = minSize,
632
+ maxSize = maxSize,
633
+ availableWidth = availableGroupWidth.toFloat()
634
+ ) { candidateSize ->
635
+ val textSizePx = candidateSize * scaledDensity
636
+ val inputW = maxOf(
637
+ measureSingleLineTextWidthPx(probeText, textSizePx, typeface) + singleLineWidthPadding,
638
+ minInputWidth
622
639
  )
640
+ (inputW +
641
+ measureSideLabelWidthPx(prefixText, textSizePx, typeface) +
642
+ measureSideLabelWidthPx(suffixText, textSizePx, typeface)).toFloat()
623
643
  }
624
644
  val heightFitSize = if (maxTextHeight <= 0) {
625
645
  minSize
@@ -838,30 +858,51 @@ class HybridAutoSizeInput(val context: ThemedReactContext) : HybridAutoSizeInput
838
858
  }
839
859
 
840
860
  private fun measureSingleLineTextWidthPx(text: String): Int {
861
+ return measureSingleLineTextWidthPx(
862
+ text,
863
+ currentFontSize * context.resources.displayMetrics.scaledDensity,
864
+ makeTypeface()
865
+ )
866
+ }
867
+
868
+ // Size-parameterized so the auto-width fit can probe candidates.
869
+ private fun measureSingleLineTextWidthPx(text: String, textSizePx: Float, typeface: Typeface): Int {
841
870
  if (text.isEmpty()) return 0
842
871
  val paint = TextPaint(Paint.ANTI_ALIAS_FLAG)
843
- paint.textSize = currentFontSize * context.resources.displayMetrics.scaledDensity
844
- paint.typeface = makeTypeface()
872
+ paint.textSize = textSizePx
873
+ paint.typeface = typeface
845
874
  return kotlin.math.ceil(paint.measureText(text).toDouble()).toInt()
846
875
  }
847
876
 
877
+ // Width a prefix/suffix label occupies at [textSizePx]. Shared by
878
+ // the auto-width fit and measureTextViewWidthPx so both agree per size.
879
+ private fun measureSideLabelWidthPx(text: String, textSizePx: Float, typeface: Typeface): Int {
880
+ if (text.isEmpty()) return 0
881
+ val paint = TextPaint(Paint.ANTI_ALIAS_FLAG)
882
+ paint.textSize = textSizePx
883
+ paint.typeface = typeface
884
+ val bounds = Rect()
885
+ paint.getTextBounds(text, 0, text.length, bounds)
886
+ val advanceWidth = kotlin.math.ceil(paint.measureText(text).toDouble()).toInt()
887
+ return maxOf(advanceWidth, bounds.width()) + sideLabelSafetyPaddingPx(textSizePx)
888
+ }
889
+
890
+ // Keep extra room for side bearings/kerning to avoid clipping on some glyphs/fonts.
891
+ private fun sideLabelSafetyPaddingPx(textSizePx: Float): Int {
892
+ val density = context.resources.displayMetrics.density
893
+ return maxOf((4f * density).toInt(), kotlin.math.ceil(textSizePx * 0.5f).toInt())
894
+ }
895
+
848
896
  private fun measureTextViewWidthPx(textView: TextView): Int {
849
897
  val content = textView.text?.toString().orEmpty()
850
898
  if (content.isEmpty()) return 0
851
- val bounds = Rect()
852
- textView.paint.getTextBounds(content, 0, content.length, bounds)
853
- val advanceWidth = kotlin.math.ceil(textView.paint.measureText(content).toDouble()).toInt()
854
- val glyphWidth = bounds.width()
855
899
  textView.measure(
856
900
  View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
857
901
  View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
858
902
  )
859
- val desiredWidth = textView.measuredWidth
860
- val density = context.resources.displayMetrics.density
861
- // Keep extra room for side bearings/kerning to avoid clipping on some glyphs/fonts.
862
- val safetyPadding = maxOf((4f * density).toInt(), kotlin.math.ceil(textView.textSize * 0.5f).toInt())
863
- val measured = maxOf(maxOf(advanceWidth, glyphWidth), desiredWidth) + safetyPadding
864
- return measured
903
+ val desiredWidth = textView.measuredWidth + sideLabelSafetyPaddingPx(textView.textSize)
904
+ val paintWidth = measureSideLabelWidthPx(content, textView.textSize, textView.typeface)
905
+ return maxOf(paintWidth, desiredWidth)
865
906
  }
866
907
 
867
908
  private fun requestInputFocus() {
@@ -0,0 +1,30 @@
1
+ package com.margelo.nitro.autosizeinput
2
+
3
+ // Font fit for contentAutoWidth mode. The input text, prefix,
4
+ // suffix and their safety padding all scale with the font, so every candidate
5
+ // size must be measured as a whole. Measuring the side labels once at the
6
+ // previous font size under-reports their width whenever the font grows (e.g.
7
+ // the send-amount fiat toggle adds a "$" prefix and drops the token suffix) and
8
+ // the layout pass then hands the input a slot narrower than its text, which
9
+ // scrolls the leading digits out of view.
10
+ internal object AutoWidthFontFit {
11
+ fun findLargestFittingSize(
12
+ minSize: Float,
13
+ maxSize: Float,
14
+ availableWidth: Float,
15
+ measureAt: (Float) -> Float
16
+ ): Float {
17
+ if (availableWidth <= 0f || maxSize <= minSize) return minSize
18
+ var low = minSize
19
+ var high = maxSize
20
+ while (high - low > 0.5f) {
21
+ val mid = (low + high) / 2f
22
+ if (measureAt(mid) <= availableWidth) {
23
+ low = mid
24
+ } else {
25
+ high = mid
26
+ }
27
+ }
28
+ return low
29
+ }
30
+ }
@@ -0,0 +1,45 @@
1
+ package com.margelo.nitro.autosizeinput
2
+
3
+ import org.junit.Assert.assertEquals
4
+ import org.junit.Assert.assertTrue
5
+ import org.junit.Test
6
+
7
+ class AutoWidthFontFitTest {
8
+ // Synthetic metrics: ten digits plus a "$" prefix whose safety padding
9
+ // scales with the font, mirroring measureTextViewWidthPx.
10
+ private fun contentWidthAt(size: Float): Float {
11
+ val text = 10 * 0.58f * size
12
+ val prefix = 0.55f * size + 0.5f * size
13
+ return text + prefix
14
+ }
15
+
16
+ @Test
17
+ fun scalesSideLabelsWithTheCandidateSize() {
18
+ val available = 400f
19
+ val size = AutoWidthFontFit.findLargestFittingSize(16f, 67f, available, ::contentWidthAt)
20
+
21
+ assertTrue(contentWidthAt(size) <= available)
22
+ assertTrue(contentWidthAt(size + 0.5f) > available)
23
+ }
24
+
25
+ @Test
26
+ fun regressionPrefixMeasuredAtOldSizeOverflows() {
27
+ // Before the fix the prefix was measured once at the previous font size
28
+ // (20sp here) and only the text was fitted, so the chosen size overflowed
29
+ // once applyFontSize scaled the prefix up as well.
30
+ val available = 400f
31
+ val stalePrefix = 0.55f * 20f + 0.5f * 20f
32
+ val staleFit = AutoWidthFontFit.findLargestFittingSize(16f, 67f, available - stalePrefix) { 10 * 0.58f * it }
33
+ assertTrue(contentWidthAt(staleFit) > available)
34
+
35
+ val fit = AutoWidthFontFit.findLargestFittingSize(16f, 67f, available, ::contentWidthAt)
36
+ assertTrue(contentWidthAt(fit) <= available)
37
+ assertTrue(fit < staleFit)
38
+ }
39
+
40
+ @Test
41
+ fun returnsMinSizeWhenNothingFits() {
42
+ assertEquals(16f, AutoWidthFontFit.findLargestFittingSize(16f, 67f, 0f, ::contentWidthAt), 0f)
43
+ assertEquals(16f, AutoWidthFontFit.findLargestFittingSize(16f, 67f, 10f, ::contentWidthAt), 0f)
44
+ }
45
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-auto-size-input",
3
- "version": "3.0.131",
3
+ "version": "3.0.132",
4
4
  "description": "Auto-sizing text input with font scaling, prefix and suffix support",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",