Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions docs/docs/guides/view-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,83 @@ function App() {
}
```

## Children

A Nitro `HybridView` can render React children, just like a normal `<View>`. They get laid out by Yoga and mounted as subviews of your native view, so the view can draw behind or around them.

<div className="side-by-side-container">
<div className="side-by-side-block">

```ts title="GradientView.nitro.ts"
export interface GradientViewProps
extends HybridViewProps {
colors: string[]
}
export type GradientView =
HybridView<GradientViewProps>
```

</div>
<div className="side-by-side-block">

```jsx title="App.tsx"
function App() {
return (
<GradientView
colors={['#FF0080', '#7928CA']}
style={{ flex: 1, padding: 24 }}
>
<Text>Welcome back</Text>
<Button title="Continue" />
</GradientView>
)
}
```

</div>
</div>

This works out of the box, there's nothing extra to set up. The only requirement is that your native view can actually hold subviews:

- On iOS, any `UIView` already can.
- On Android, return a `NitroViewGroup` (or a subclass) from `view`. It leaves children where Fabric placed them. A regular `ViewGroup` would run its own layout and move them around, and a plain `View` can't hold children at all, so Nitro throws an error.

<div className="side-by-side-container">
<div className="side-by-side-block">

```swift title="HybridGradientView.swift"
class HybridGradientView: HybridGradientViewSpec {
let view = GradientUIView()

func setColors(_ colors: [String]) {
view.gradientLayer.colors = colors.map {
UIColor(hex: $0).cgColor
}
}
}
```

</div>
<div className="side-by-side-block">

```kotlin title="HybridGradientView.kt"
class HybridGradientView(context: ThemedReactContext):
HybridGradientViewSpec() {
override val view = GradientView(context)

override fun setColors(colors: Array<String>) {
view.setGradientColors(colors.map(Color::parseColor))
}
}
```

</div>
</div>

:::note Styling
A Hybrid View only forwards layout to its native view. It won't apply visual style props like `backgroundColor`, `borderRadius` or `boxShadow`, since the native view decides how it looks. To style one, wrap it in a normal `<View>` and add `overflow: 'hidden'` to clip.
:::

## 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!
Expand Down
4 changes: 2 additions & 2 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2239,7 +2239,7 @@ SPEC CHECKSUMS:
React: e2dc35338068bbd299c66f043ae0d7f25de8499e
React-callinvoker: 28b25d21b124c26cebaea713ba7d801b9351dc48
React-Core: 02ed7d2ffb70437bdf2aba074a13078a7b0b9ff0
React-Core-prebuilt: 72454b4caa0309d15c45de529b1c5a0e4607cf9a
React-Core-prebuilt: fb974d1664c98b4ece04886442bbad2fa51c80eb
React-CoreModules: b3a5a42dadcde3b5d47b325bd912eb2ced89e146
React-cxxreact: fe8f88dda044e5905e99a00f41b7a874c3908716
React-debug: 92944dc4d89f56d640e75498266cbde557a48189
Expand Down Expand Up @@ -2303,7 +2303,7 @@ SPEC CHECKSUMS:
ReactAppDependencyProvider: 25c9c516839be2c5e3d3344f95dc7da5f7e63fc2
ReactCodegen: 0f100aa6334186385a43f0dd13d63efc6805ea55
ReactCommon: 7dfc3250793bf36cf221096ff59e1179e13eef7f
ReactNativeDependencies: 86c5427d73b954c0671c6ab9691d486b196595b6
ReactNativeDependencies: 589c57612aa76b4cf9130967abc61b82999676ef
RNScreens: 991cc417cd396602a6cf59a42139e5a9d91462a9
Yoga: 77dfa8673de2874e1855002ae59c68b8be9b007b

Expand Down
65 changes: 64 additions & 1 deletion example/src/screens/ViewScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import * as React from 'react'

import { StyleSheet, View, Text, Button, Platform } from 'react-native'
import { StyleSheet, View, Text, Button, Pressable, Platform } from 'react-native'
import { callback, NitroModules } from 'react-native-nitro-modules'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { useColors } from '../useColors'
import {
GradientView,
HybridTestObjectSwiftKotlin,
RecyclableTestView,
TestView,
Expand Down Expand Up @@ -76,6 +77,27 @@ export function ViewScreenImpl() {
<Text style={styles.buildTypeText}>{NitroModules.buildType}</Text>
</View>

<View style={styles.gradientBannerWrapper}>
<GradientView
colors={['#FF0080', '#7928CA']}
style={styles.gradientBanner}
>
<Text style={styles.gradientTitle}>Nitro GradientView</Text>
<Text style={styles.gradientSubtitle}>
These children are mounted on top of a native gradient
</Text>
<Pressable
onPress={() => console.log('Gradient child button tapped!')}
style={({ pressed }) => [
styles.tapChild,
pressed && styles.tapChildPressed,
]}
>
<Text style={styles.tapChildText}>Tap a child</Text>
</Pressable>
</GradientView>
</View>

<View style={styles.resultContainer}>
<View style={[styles.viewShadow]}>
<View style={[styles.viewBorder, { borderColor: colors.foreground }]}>
Expand Down Expand Up @@ -128,6 +150,47 @@ const styles = StyleSheet.create({
}),
fontWeight: 'bold',
},
gradientBannerWrapper: {
marginHorizontal: 15,
marginBottom: 15,
borderRadius: 16,
overflow: 'hidden',
},
gradientBanner: {
padding: 16,
gap: 8,
},
gradientTitle: {
color: 'white',
fontSize: 18,
fontWeight: 'bold',
},
gradientSubtitle: {
color: 'white',
fontSize: 13,
},
tapChild: {
alignSelf: 'flex-start',
marginTop: 4,
backgroundColor: 'rgba(255, 255, 255, 0.95)',
paddingVertical: 10,
paddingHorizontal: 20,
borderRadius: 999,
shadowColor: 'black',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 4,
elevation: 3,
},
tapChildPressed: {
opacity: 0.85,
transform: [{ scale: 0.96 }],
},
tapChildText: {
color: '#7928CA',
fontSize: 14,
fontWeight: '700',
},
segmentedControl: {
minWidth: 180,
},
Expand Down
33 changes: 32 additions & 1 deletion packages/nitrogen/src/views/kotlin/KotlinHybridViewManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ ${createFileMetadataString(`${manager}.kt`)}
package ${javaSubNamespace}

import android.view.View
import android.view.ViewGroup
import com.facebook.react.uimanager.IViewGroupManager
import com.facebook.react.uimanager.ReactStylesDiffMap
import com.facebook.react.uimanager.SimpleViewManager
import com.facebook.react.uimanager.StateWrapper
Expand All @@ -55,7 +57,7 @@ import ${javaNamespace}.*
/**
* Represents the React Native \`ViewManager\` for the "${spec.name}" Nitro HybridView.
*/
public class ${manager}: SimpleViewManager<View>() {
public class ${manager}: SimpleViewManager<View>(), IViewGroupManager<View> {
init {
if (RecyclableView::class.java.isAssignableFrom(${viewImplementation}::class.java)) {
// Enable view recycling
Expand Down Expand Up @@ -113,6 +115,35 @@ public class ${manager}: SimpleViewManager<View>() {
private fun getHybridView(view: View): ${viewImplementation}? {
return view.getTag(associated_hybrid_view_tag) as? ${viewImplementation}
}

override fun needsCustomLayoutForChildren(): Boolean {
// false: Fabric positions children directly, so we don't lay them out.
return false
}

override fun addView(parent: View, child: View, index: Int) {
asViewGroup(parent).addView(child, index)
}

override fun getChildAt(parent: View, index: Int): View? {
return asViewGroup(parent).getChildAt(index)
}

override fun getChildCount(parent: View): Int {
return (parent as? ViewGroup)?.childCount ?: 0
}

override fun removeViewAt(parent: View, index: Int) {
asViewGroup(parent).removeViewAt(index)
}

private fun asViewGroup(view: View): ViewGroup {
if (view is ViewGroup) {
return view
}
val className = view.javaClass.simpleName
throw IllegalStateException("Nitro view \\"${spec.name}\\" received React children, but its native view ($className) is not a ViewGroup! To render children, return a \`NitroViewGroup\` (or another \`android.view.ViewGroup\`) from your HybridView's \`view\`.")
}
}
`.trim()

Expand Down
25 changes: 25 additions & 0 deletions packages/nitrogen/src/views/swift/SwiftHybridViewManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,31 @@ using namespace ${namespace}::views;
[self setContentView:view];
}

- (void) mountChildComponentView:(UIView<RCTComponentViewProtocol>*)childComponentView index:(NSInteger)index {
// Mount children inside the contentView, not as siblings of it (fall back if there's none yet).
UIView* container = self.contentView;
if (container == nil) {
[super mountChildComponentView:childComponentView index:index];
return;
}
[container insertSubview:childComponentView atIndex:index];
}

- (void) unmountChildComponentView:(UIView<RCTComponentViewProtocol>*)childComponentView index:(NSInteger)index {
if (childComponentView.superview == nil) {
[super unmountChildComponentView:childComponentView index:index];
return;
}
[childComponentView removeFromSuperview];
}

- (void) updateLayoutMetrics:(const facebook::react::LayoutMetrics&)layoutMetrics
oldLayoutMetrics:(const facebook::react::LayoutMetrics&)oldLayoutMetrics {
[super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
// Make the contentView fill the component, else children would be double-inset by padding/border.
self.contentView.frame = self.bounds;
}

- (void) updateProps:(const std::shared_ptr<const react::Props>&)props
oldProps:(const std::shared_ptr<const react::Props>&)oldProps {
// 1. Downcast props
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.margelo.nitro.views

import android.annotation.SuppressLint
import android.content.Context
import android.view.ViewGroup

/**
* Base [ViewGroup] for [HybridView]s that render React children. Return one of these
* (or a subclass) from your `HybridView`'s `view` when it should host children.
*/
public open class NitroViewGroup(context: Context) : ViewGroup(context) {
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
// No-op: React Native positions each child.
}

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
setMeasuredDimension(
MeasureSpec.getSize(widthMeasureSpec),
MeasureSpec.getSize(heightMeasureSpec),
)
}

@SuppressLint("MissingSuperCall")
override fun requestLayout() {
// No-op: React Native drives layout, not Android.
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.margelo.nitro.test

import android.content.Context
import android.graphics.Canvas
import android.graphics.LinearGradient
import android.graphics.Paint
import android.graphics.Shader
import androidx.annotation.Keep
import androidx.core.graphics.toColorInt
import com.facebook.proguard.annotations.DoNotStrip
import com.facebook.react.uimanager.ThemedReactContext
import com.margelo.nitro.views.NitroViewGroup

class GradientView(context: Context) : NitroViewGroup(context) {
private var gradientColors: IntArray = intArrayOf()
private val paint = Paint()

init {
setWillNotDraw(false)
}

fun setGradientColors(colors: IntArray) {
gradientColors = colors
updateShader()
invalidate()
}

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
updateShader()
}

private fun updateShader() {
paint.shader = if (gradientColors.size >= 2 && width > 0 && height > 0) {
LinearGradient(
0f,
0f,
width.toFloat(),
height.toFloat(),
gradientColors,
null,
Shader.TileMode.CLAMP,
)
} else {
null
}
}

override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (paint.shader != null) {
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)
}
}
}

@Keep
@DoNotStrip
class HybridGradientView(
val context: ThemedReactContext,
) : HybridGradientViewSpec() {
// View
override val view: GradientView = GradientView(context)

// Props
override var colors: Array<String> = arrayOf()
set(value) {
field = value
val parsed = value.mapNotNull { runCatching { it.toColorInt() }.getOrNull() }
view.setGradientColors(parsed.toIntArray())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.model.ReactModuleInfoProvider
import com.facebook.react.uimanager.ViewManager
import com.margelo.nitro.test.views.HybridGradientViewManager
import com.margelo.nitro.test.views.HybridRecyclableTestViewManager
import com.margelo.nitro.test.views.HybridTestViewManager

Expand All @@ -20,6 +21,7 @@ class NitroTestPackage : BaseReactPackage() {
val viewManagers = ArrayList<ViewManager<*, *>>()
viewManagers.add(HybridTestViewManager())
viewManagers.add(HybridRecyclableTestViewManager())
viewManagers.add(HybridGradientViewManager())
return viewManagers
}

Expand Down
Loading