feat(mobile): implement custom tab bar and item pressable components on android native (#3613)
* feat(mobile): implement custom tab bar and item pressable components on android native
Adds native Android implementations for custom UI components previously available on iOS:
- **Custom Tab Bar:**
- Implements `TabBarRootView` using `ViewPager2` and a custom `CustomTabBarView` (LinearLayout) to host RN-rendered tab items (`TabBarPortalView`).
- Provides `TabScreenView` as a container for page content within Fragments managed by `TabBarPagerAdapter`.
- Includes `TabBarModule`, `TabScreenModule`, and `TabBarPortalModule` for Expo autolinking and JS bridging (props: `selectedIndex`, event: `onTabIndexChange`, function: `switchTab`).
- This approach mirrors the iOS version by avoiding the native `TabLayout` and allowing full customization of the tab bar via React Native components.
- **Item Pressable:**
- Implements `ItemPressableView` extending `ExpoView`.
* Provides native touch feedback using Android's foreground ripple (`selectableItemBackground`) by default.
* Includes a prop `touchHighlight` to enable/disable feedback.
* Fires `onItemPress` event on click/tap.
* Includes `ItemPressableModule` for Expo integration.
* fix(mobile): Adjust content view `padding` to prevent overlap with Bottom Bar
* feat(mobile): implement native tab bar components and improve tab switching logic
This commit is contained in:
parent
9873e9af0f
commit
ad3ac4926a
|
|
@ -28,6 +28,11 @@ if (useManagedAndroidSdkVersions) {
|
|||
minSdkVersion safeExtGet("minSdkVersion", 21)
|
||||
targetSdkVersion safeExtGet("targetSdkVersion", 34)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "androidx.fragment:fragment:1.8.6"
|
||||
implementation "androidx.viewpager2:viewpager2:1.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
package expo.modules.follownative.itempressable
|
||||
|
||||
import expo.modules.kotlin.modules.Module
|
||||
import expo.modules.kotlin.modules.ModuleDefinition
|
||||
|
||||
class ItemPressableModule : Module() {
|
||||
override fun definition() = ModuleDefinition {
|
||||
Name("ItemPressable")
|
||||
|
||||
View(ItemPressableView::class) {
|
||||
Prop("touchHighlight") { view: ItemPressableView, enabled: Boolean ->
|
||||
view.setTouchHighlight(enabled)
|
||||
}
|
||||
|
||||
Events("onItemPress")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package expo.modules.follownative.itempressable
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.RippleDrawable
|
||||
import android.os.Build
|
||||
import android.util.TypedValue
|
||||
import android.view.MotionEvent
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import expo.modules.kotlin.AppContext
|
||||
import expo.modules.kotlin.viewevent.EventDispatcher
|
||||
import expo.modules.kotlin.views.ExpoView
|
||||
import androidx.core.view.isNotEmpty
|
||||
|
||||
class ItemPressableView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
|
||||
|
||||
val onItemPress by EventDispatcher()
|
||||
|
||||
private var touchHighlightEnabled: Boolean = true
|
||||
private var originalBackground: Drawable? = null
|
||||
private var isPressedDown = false
|
||||
|
||||
private var rippleDrawable: RippleDrawable? = null
|
||||
private val highlightColor = Color.argb(40, 0, 0, 0)
|
||||
|
||||
init {
|
||||
isClickable = true
|
||||
isFocusable = true
|
||||
|
||||
originalBackground = background
|
||||
|
||||
setupRipple()
|
||||
}
|
||||
|
||||
fun setTouchHighlight(enabled: Boolean) {
|
||||
touchHighlightEnabled = enabled
|
||||
if (!enabled) {
|
||||
foreground = null
|
||||
} else {
|
||||
setupRipple()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupRipple() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && touchHighlightEnabled) {
|
||||
val typedValue = TypedValue()
|
||||
context.theme.resolveAttribute(android.R.attr.selectableItemBackground, typedValue, true)
|
||||
foreground = ContextCompat.getDrawable(context, typedValue.resourceId)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override fun performClick(): Boolean {
|
||||
super.performClick() // Handles accessibility events, sound effects etc.
|
||||
onItemPress(emptyMap()) // Dispatch the event (with no arg) to React Native
|
||||
return true
|
||||
}
|
||||
|
||||
init {
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
if (isNotEmpty()) {
|
||||
val child = getChildAt(0)
|
||||
measureChild(child, widthMeasureSpec, heightMeasureSpec)
|
||||
setMeasuredDimension(child.measuredWidth, child.measuredHeight)
|
||||
} else {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
|
||||
if (isNotEmpty()) {
|
||||
val child = getChildAt(0)
|
||||
// Layout the child to fill this container
|
||||
child.layout(0, 0, r - l, b - t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package expo.modules.follownative.tabbar
|
||||
|
||||
import expo.modules.kotlin.modules.Module
|
||||
import expo.modules.kotlin.modules.ModuleDefinition
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class TabBarModule : Module() {
|
||||
override fun definition() = ModuleDefinition {
|
||||
Name("TabBarRoot")
|
||||
|
||||
View(TabBarRootView::class) {
|
||||
Prop("selectedIndex") { view: TabBarRootView, index: Int ->
|
||||
view.setSelectedIndex(index)
|
||||
}
|
||||
|
||||
Events("onTabIndexChange")
|
||||
}
|
||||
|
||||
AsyncFunction("switchTab") { view: TabBarRootView, index: Int ->
|
||||
appContext.mainQueue.launch {
|
||||
view.setSelectedIndex(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package expo.modules.follownative.tabbar
|
||||
|
||||
import expo.modules.kotlin.modules.Module
|
||||
import expo.modules.kotlin.modules.ModuleDefinition
|
||||
|
||||
class TabBarPortalModule : Module() {
|
||||
override fun definition() = ModuleDefinition {
|
||||
Name("TabBarPortal")
|
||||
|
||||
View(TabBarPortalView::class) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package expo.modules.follownative.tabbar
|
||||
|
||||
import android.content.Context
|
||||
import expo.modules.kotlin.AppContext
|
||||
import expo.modules.kotlin.views.ExpoView
|
||||
import androidx.core.view.isNotEmpty
|
||||
|
||||
class TabBarPortalView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
package expo.modules.follownative.tabbar
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import expo.modules.kotlin.AppContext
|
||||
import expo.modules.kotlin.viewevent.EventDispatcher
|
||||
import expo.modules.kotlin.views.ExpoView
|
||||
|
||||
class TabBarRootView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
|
||||
|
||||
override val shouldUseAndroidLayout: Boolean = true
|
||||
|
||||
private val container = FrameLayout(context).apply {
|
||||
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
||||
}
|
||||
|
||||
internal val viewPager: ViewPager2 = ViewPager2(context).apply {
|
||||
id = View.generateViewId()
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
offscreenPageLimit = 1
|
||||
isUserInputEnabled = false
|
||||
}
|
||||
|
||||
private val tabFragments = mutableListOf<TabScreenFragment>()
|
||||
private var pagerAdapter: FragmentStateAdapter? = null
|
||||
private val onTabIndexChange by EventDispatcher<Map<String, Int>>()
|
||||
|
||||
init {
|
||||
super.addView(container)
|
||||
|
||||
container.addView(viewPager)
|
||||
|
||||
setupAdapter()
|
||||
|
||||
viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
super.onPageSelected(position)
|
||||
onTabIndexChange(mapOf("index" to position))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun setupAdapter() {
|
||||
val activity = appContext.activityProvider?.currentActivity as? FragmentActivity
|
||||
if (activity != null) {
|
||||
pagerAdapter = object : FragmentStateAdapter(activity) {
|
||||
override fun getItemCount(): Int = tabFragments.size
|
||||
override fun createFragment(position: Int): Fragment = tabFragments[position]
|
||||
}
|
||||
viewPager.adapter = pagerAdapter
|
||||
} else {
|
||||
Log.e("TabBarRootView", "FragmentActivity is null. Cannot set up ViewPager2 adapter.")
|
||||
}
|
||||
}
|
||||
|
||||
fun addTabScreenView(screenView: TabScreenView, index: Int) {
|
||||
val fragment = TabScreenFragment.newInstance(screenView)
|
||||
if (index >= tabFragments.size) {
|
||||
tabFragments.add(fragment)
|
||||
} else {
|
||||
tabFragments.add(index, fragment)
|
||||
}
|
||||
pagerAdapter?.notifyItemInserted(index)
|
||||
}
|
||||
|
||||
fun removeTabScreenView(screenView: TabScreenView) {
|
||||
val index = tabFragments.indexOfFirst { it.containsView(screenView) }
|
||||
if (index != -1) {
|
||||
tabFragments.removeAt(index)
|
||||
pagerAdapter?.notifyItemRemoved(index)
|
||||
}
|
||||
}
|
||||
|
||||
fun addPortalView(portalView: TabBarPortalView) {
|
||||
val layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
(64 * resources.displayMetrics.density).toInt()
|
||||
).apply {
|
||||
gravity = Gravity.BOTTOM
|
||||
}
|
||||
|
||||
portalView.layoutParams = layoutParams
|
||||
|
||||
container.addView(portalView)
|
||||
|
||||
viewPager.layoutParams = (viewPager.layoutParams as FrameLayout.LayoutParams).apply {
|
||||
bottomMargin = (64 * resources.displayMetrics.density).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
fun removePortalView(portalView: TabBarPortalView) {
|
||||
container.removeView(portalView)
|
||||
}
|
||||
|
||||
fun setSelectedIndex(index: Int) {
|
||||
if (index >= 0 && index < tabFragments.size) {
|
||||
viewPager.setCurrentItem(index, false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun addView(child: View?, index: Int) {
|
||||
if (child is TabScreenView) {
|
||||
addTabScreenView(child, index)
|
||||
} else if (child is TabBarPortalView) {
|
||||
addPortalView(child)
|
||||
} else {
|
||||
super.addView(child, index)
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeView(child: View?) {
|
||||
if (child is TabScreenView) {
|
||||
removeTabScreenView(child)
|
||||
} else if (child is TabBarPortalView) {
|
||||
removePortalView(child)
|
||||
} else {
|
||||
super.removeView(child)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package expo.modules.follownative.tabbar
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
|
||||
class TabScreenFragment : Fragment() {
|
||||
private var screenView: TabScreenView? = null
|
||||
|
||||
override fun onCreateView(inflater: android.view.LayoutInflater, container: android.view.ViewGroup?, savedInstanceState: android.os.Bundle?): View? {
|
||||
if (screenView?.parent != null) {
|
||||
(screenView?.parent as? android.view.ViewGroup)?.removeView(screenView)
|
||||
}
|
||||
return screenView
|
||||
}
|
||||
|
||||
fun containsView(view: TabScreenView): Boolean {
|
||||
return screenView == view
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance(view: TabScreenView): TabScreenFragment {
|
||||
val fragment = TabScreenFragment()
|
||||
fragment.screenView = view
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package expo.modules.follownative.tabbar
|
||||
|
||||
import expo.modules.kotlin.modules.Module
|
||||
import expo.modules.kotlin.modules.ModuleDefinition
|
||||
|
||||
class TabScreenModule : Module() {
|
||||
override fun definition() = ModuleDefinition {
|
||||
Name("TabScreen")
|
||||
|
||||
View(TabScreenView::class) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package expo.modules.follownative.tabbar
|
||||
|
||||
import android.content.Context
|
||||
import expo.modules.kotlin.AppContext
|
||||
import expo.modules.kotlin.views.ExpoView
|
||||
|
||||
|
||||
class TabScreenView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
|
||||
|
||||
}
|
||||
|
|
@ -17,6 +17,11 @@
|
|||
]
|
||||
},
|
||||
"android": {
|
||||
"modules": []
|
||||
"modules": [
|
||||
"expo.modules.follownative.tabbar.TabBarModule",
|
||||
"expo.modules.follownative.tabbar.TabScreenModule",
|
||||
"expo.modules.follownative.tabbar.TabBarPortalModule",
|
||||
"expo.modules.follownative.itempressable.ItemPressableModule"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
import { requireNativeView } from "expo"
|
||||
import { useSetAtom } from "jotai"
|
||||
import { useContext } from "react"
|
||||
import type { ViewProps } from "react-native"
|
||||
import { StyleSheet, View } from "react-native"
|
||||
|
||||
import { BottomTabContext } from "./BottomTabContext"
|
||||
|
||||
const TabBarPortalNative = requireNativeView<ViewProps>("TabBarPortal")
|
||||
|
||||
export const TabBarPortal = ({ children }: { children: React.ReactNode }) => {
|
||||
const { tabHeightAtom } = useContext(BottomTabContext)
|
||||
const setTabHeight = useSetAtom(tabHeightAtom)
|
||||
return (
|
||||
// Use native view
|
||||
<TabBarPortalNative style={styles.container}>
|
||||
<View
|
||||
onLayout={(e) => {
|
||||
setTabHeight(e.nativeEvent.layout.height)
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
</TabBarPortalNative>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
},
|
||||
})
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import { requireNativeView } from "expo"
|
||||
import { useAtom } from "jotai"
|
||||
import type { FC, PropsWithChildren } from "react"
|
||||
import * as React from "react"
|
||||
import { useCallback, useContext, useMemo } from "react"
|
||||
import type { NativeSyntheticEvent, ViewProps } from "react-native"
|
||||
import { StyleSheet } from "react-native"
|
||||
|
||||
import { BottomTabContext } from "./BottomTabContext"
|
||||
import { TabScreen } from "./TabScreen"
|
||||
|
||||
const TabBarRoot = requireNativeView<
|
||||
{
|
||||
onTabIndexChange: (e: NativeSyntheticEvent<{ index: number }>) => void
|
||||
selectedIndex: number
|
||||
} & ViewProps
|
||||
>("TabBarRoot")
|
||||
|
||||
export const TabRoot: FC<PropsWithChildren> = ({ children }) => {
|
||||
const { currentIndexAtom } = useContext(BottomTabContext)
|
||||
const [tabIndex, setTabIndex] = useAtom(currentIndexAtom)
|
||||
|
||||
const MapChildren = useMemo(() => {
|
||||
let cnt = 0
|
||||
return React.Children.map(children, (child) => {
|
||||
if (typeof child === "object" && child && "type" in child && child.type === TabScreen) {
|
||||
return React.cloneElement(child, {
|
||||
tabScreenIndex: cnt++,
|
||||
})
|
||||
}
|
||||
return child
|
||||
})
|
||||
}, [children])
|
||||
|
||||
return (
|
||||
<TabBarRoot
|
||||
style={StyleSheet.absoluteFill}
|
||||
onTabIndexChange={useCallback(
|
||||
(e) => {
|
||||
setTabIndex(e.nativeEvent.index)
|
||||
},
|
||||
[setTabIndex],
|
||||
)}
|
||||
selectedIndex={tabIndex}
|
||||
>
|
||||
{MapChildren}
|
||||
</TabBarRoot>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
import { requireNativeView } from "expo"
|
||||
import { atom, useAtom, useAtomValue, useSetAtom } from "jotai"
|
||||
import type { FC, PropsWithChildren } from "react"
|
||||
import { useContext, useEffect, useMemo } from "react"
|
||||
import type { ViewProps } from "react-native"
|
||||
import { StyleSheet } from "react-native"
|
||||
|
||||
import { WrappedScreenItem } from "../WrappedScreenItem"
|
||||
import { BottomTabContext } from "./BottomTabContext"
|
||||
import { LifecycleEvents, ScreenNameRegister } from "./shared"
|
||||
import type { TabScreenContextType } from "./TabScreenContext"
|
||||
import { TabScreenContext } from "./TabScreenContext"
|
||||
import type { TabScreenComponent, TabScreenProps } from "./types"
|
||||
|
||||
const TabScreenNative = requireNativeView<ViewProps>("TabScreen")
|
||||
|
||||
export const TabScreen: FC<PropsWithChildren<Omit<TabScreenProps, "tabScreenIndex">>> = ({
|
||||
children,
|
||||
identifier,
|
||||
...props
|
||||
}) => {
|
||||
const { tabScreenIndex } = props as any as TabScreenProps
|
||||
|
||||
const {
|
||||
loadedableIndexAtom,
|
||||
currentIndexAtom,
|
||||
tabScreensAtom: tabScreens,
|
||||
} = useContext(BottomTabContext)
|
||||
|
||||
const setTabScreens = useSetAtom(tabScreens)
|
||||
|
||||
const mergedProps = useMemo(() => {
|
||||
const propsFromChildren: Partial<TabScreenProps> = {}
|
||||
if (children && typeof children === "object") {
|
||||
const childType = (children as any).type as TabScreenComponent
|
||||
if ("tabBarIcon" in childType) {
|
||||
propsFromChildren.renderIcon = childType.tabBarIcon
|
||||
}
|
||||
if ("title" in childType) {
|
||||
propsFromChildren.title = childType.title
|
||||
}
|
||||
if ("lazy" in childType) {
|
||||
propsFromChildren.lazy = childType.lazy
|
||||
}
|
||||
if ("identifier" in childType && typeof childType.identifier === "string") {
|
||||
propsFromChildren.identifier = childType.identifier
|
||||
}
|
||||
}
|
||||
return {
|
||||
...propsFromChildren,
|
||||
...props,
|
||||
identifier: identifier ?? propsFromChildren.identifier,
|
||||
}
|
||||
}, [children, props, identifier])
|
||||
|
||||
useEffect(() => {
|
||||
setTabScreens((prev) => [
|
||||
...prev,
|
||||
{
|
||||
...mergedProps,
|
||||
tabScreenIndex,
|
||||
},
|
||||
])
|
||||
|
||||
return () => {
|
||||
setTabScreens((prev) =>
|
||||
prev.filter((tabScreen) => tabScreen.tabScreenIndex !== tabScreenIndex),
|
||||
)
|
||||
}
|
||||
}, [mergedProps, setTabScreens, tabScreenIndex])
|
||||
|
||||
const currentSelectedIndex = useAtomValue(currentIndexAtom)
|
||||
|
||||
const isSelected = useMemo(
|
||||
() => currentSelectedIndex === tabScreenIndex,
|
||||
[currentSelectedIndex, tabScreenIndex],
|
||||
)
|
||||
|
||||
const [loadedableIndexSet, setLoadedableIndex] = useAtom(loadedableIndexAtom)
|
||||
|
||||
const isLoadedBefore = loadedableIndexSet.has(tabScreenIndex)
|
||||
useEffect(() => {
|
||||
if (isSelected) {
|
||||
setLoadedableIndex((prev) => {
|
||||
prev.add(tabScreenIndex)
|
||||
return new Set(prev)
|
||||
})
|
||||
}
|
||||
}, [setLoadedableIndex, tabScreenIndex, isSelected])
|
||||
|
||||
const ctxValue = useMemo<TabScreenContextType>(
|
||||
() => ({
|
||||
tabScreenIndex,
|
||||
identifierAtom: atom(mergedProps.identifier ?? ""),
|
||||
titleAtom: atom(mergedProps.title),
|
||||
}),
|
||||
[tabScreenIndex, mergedProps.title, mergedProps.identifier],
|
||||
)
|
||||
const shouldLoadReact = mergedProps.lazy ? isSelected || isLoadedBefore : true
|
||||
|
||||
const render = __DEV__ ? isSelected : true
|
||||
|
||||
return (
|
||||
<TabScreenNative style={StyleSheet.absoluteFill}>
|
||||
<TabScreenContext.Provider value={ctxValue}>
|
||||
{shouldLoadReact && render && (
|
||||
<WrappedScreenItem screenId={`tab-screen-${tabScreenIndex}`}>
|
||||
{children}
|
||||
<ScreenNameRegister />
|
||||
<LifecycleEvents isSelected={isSelected} />
|
||||
</WrappedScreenItem>
|
||||
)}
|
||||
</TabScreenContext.Provider>
|
||||
</TabScreenNative>
|
||||
)
|
||||
}
|
||||
|
|
@ -35,7 +35,7 @@ export function Settings() {
|
|||
ref={scrollViewRef}
|
||||
style={{ paddingTop: insets.top }}
|
||||
className="bg-system-grouped-background flex-1"
|
||||
contentViewClassName="-mt-24"
|
||||
contentViewClassName="-mt-24 pb-36"
|
||||
>
|
||||
<UserHeaderBanner scrollY={screenContext.reAnimatedScrollY} userId={whoami?.id} />
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue