diff --git a/docs/docs/guides/view-components.md b/docs/docs/guides/view-components.md index b44f195d8e..ff28b9812f 100644 --- a/docs/docs/guides/view-components.md +++ b/docs/docs/guides/view-components.md @@ -158,6 +158,70 @@ function App() { } ``` +## Self-measurement (`MeasurableView`) + +By default a `HybridView` is sized by its children or by explicit style props. To instead size a view to its **own content** - a `` that grows to fit its string, a chart that derives a height from its data - conform your native implementation to `MeasurableView`. Nitro then makes the view a measurable Yoga **leaf**: Yoga calls your `measureContent` during layout and uses the returned `Size` as the intrinsic size, so no explicit `height` is needed. Like [`RecyclableView`](#recycling) it's a native opt-in, so **your `.nitro.ts` spec is unchanged**: + +```ts title="NitroTextView.nitro.ts" +export interface NitroTextViewProps extends HybridViewProps { + text: string + fontSize: number +} +export type NitroTextView = HybridView +``` + + + + ```swift title="HybridNitroTextView.swift" + class HybridNitroTextView: HybridNitroTextViewSpec, MeasurableView { + let view = UILabel() + var text: String = "" { didSet { view.text = text } } + var fontSize: Double = 17 { didSet { view.font = .systemFont(ofSize: fontSize) } } + + // `static` - no view instance needed. + // highlight-next-line + static func measureContent(props: NitroTextViewProps, layoutContext: LayoutContext, layoutConstraints: LayoutConstraints) -> Size { + // Measure `props.text` within `layoutConstraints.maximumSize` (off-thread, e.g. NSAttributedString.boundingRect). + return Size(width: measuredWidth, height: measuredHeight) + } + } + ``` + + + ```kotlin title="HybridNitroTextView.kt" + class HybridNitroTextView(context: Context) : HybridNitroTextViewSpec() { + override val view = TextView(context) + override var text: String = "" /* set { view.text = … } */ + override var fontSize: Double = 17.0 /* set { view.textSize = … } */ + + // Kotlin interfaces can't require statics, so measureContent lives on the companion. + companion object : MeasurableView { + override fun measureContent(props: NitroTextViewProps, layoutContext: LayoutContext, layoutConstraints: LayoutConstraints): Size { + // Measure `props.text` within `layoutConstraints.maximumSize` (off-thread, e.g. StaticLayout). + return Size(measuredWidth, measuredHeight) + } + } + } + ``` + + + +Now `` grows to fit its text - no `height` in its style: + +```jsx + +``` + +Because Yoga runs `measureContent` on the shadow thread during layout, it has a few hard rules: + +- **No view, pure function.** Never touch your native view inside it (it may not exist yet, and runs on another thread). Compute the size from `props` only - Yoga caches the result keyed on `props` + constraints. Use whatever measurement fits your content and is safe off the main thread (text → `boundingRect`/`StaticLayout`, an image → its source dimensions, …). +- **It's a Yoga leaf**, so a measurable view **can't also host React children** - the two are mutually exclusive (like RN's ``). +- `maximumSize.width`/`.height` is `.infinity` on an unconstrained axis (return your intrinsic size) and finite when bounded (measure within it, e.g. to wrap text). + +:::caution Not every view can self-measure +The size must be derivable from `props`, off the main thread, with no mounted view. Content that can only be sized on the main thread or via a live view's layout pass (UIKit `systemLayoutSizeFitting`, Android `View.measure()`) doesn't fit - use flexbox + explicit sizing, or precompute the size and pass it in as props. +::: + ## Props Since every `HybridView` is also a `HybridObject`, you can use any type that Nitro supports as a property - including custom types (`interface`), `ArrayBuffer`, and even other `HybridObject`s! diff --git a/example/src/screens/ViewScreen.tsx b/example/src/screens/ViewScreen.tsx index c17b84864d..e23d4f04aa 100644 --- a/example/src/screens/ViewScreen.tsx +++ b/example/src/screens/ViewScreen.tsx @@ -5,6 +5,7 @@ import { callback, NitroModules } from 'react-native-nitro-modules' import { useSafeAreaInsets } from 'react-native-safe-area-context' import { useColors } from '../useColors' import { + MeasuredView, HybridTestObjectSwiftKotlin, RecyclableTestView, TestView, @@ -14,6 +15,30 @@ import { useIsFocused } from '@react-navigation/native' const VIEWS_X = 15 const VIEWS_Y = 15 + +const MeasuredViewImpl = () => { + const [longText, setLongText] = React.useState(false) + const measuredText = longText + ? 'MeasuredView sizes its own height from this text a long string that wraps onto several lines, so the bordered box grows to fit it. Tap the button to shrink it.' + : 'MeasuredView: tap below to grow.' + + return ( + + + +