From b6ee1d37b637e271b2f7f082db17a283e4b07335 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Fri, 10 Jan 2025 15:45:35 +0100 Subject: [PATCH 1/9] feat: take whole screen, refactor measurements --- .../main/java/com/rcttabview/RCTTabView.kt | 79 ++++++++++++++++--- .../java/com/rcttabview/RCTTabViewImpl.kt | 34 ++------ .../rcttabview/events/OnNativeLayoutEvent.kt | 25 ++++++ .../{ => events}/PageSelectedEvent.kt | 2 +- .../{ => events}/TabLongPressedEvent.kt | 2 +- .../android/src/oldarch/RCTTabViewManager.kt | 61 +++----------- .../react-native-bottom-tabs/src/TabView.tsx | 3 +- .../src/TabViewAdapter.android.tsx | 17 +--- 8 files changed, 118 insertions(+), 105 deletions(-) create mode 100644 packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/events/OnNativeLayoutEvent.kt rename packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/{ => events}/PageSelectedEvent.kt (96%) rename packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/{ => events}/TabLongPressedEvent.kt (95%) diff --git a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt index 08ef3cc0..95161ea0 100644 --- a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt +++ b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt @@ -3,6 +3,7 @@ package com.rcttabview import android.annotation.SuppressLint import android.content.Context import android.content.res.ColorStateList +import android.graphics.Color import android.graphics.Typeface import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable @@ -10,10 +11,12 @@ import android.os.Build import android.util.Log import android.util.TypedValue import android.view.Choreographer +import android.view.Gravity import android.view.HapticFeedbackConstants import android.view.MenuItem import android.view.View import android.view.ViewGroup +import android.widget.FrameLayout import android.widget.TextView import androidx.appcompat.content.res.AppCompatResources import coil3.ImageLoader @@ -25,14 +28,20 @@ import com.facebook.react.views.text.ReactTypefaceUtils import com.google.android.material.bottomnavigation.BottomNavigationView import coil3.request.ImageRequest import coil3.svg.SvgDecoder +import com.google.android.material.navigation.NavigationBarView.LABEL_VISIBILITY_AUTO +import com.google.android.material.navigation.NavigationBarView.LABEL_VISIBILITY_LABELED +import com.google.android.material.navigation.NavigationBarView.LABEL_VISIBILITY_UNLABELED -class ReactBottomNavigationView(context: Context) : BottomNavigationView(context) { +class ReactBottomNavigationView(context: Context) : FrameLayout(context) { + private val bottomNavigation = BottomNavigationView(context) + private val layoutHolder = FrameLayout(context) private val iconSources: MutableMap = mutableMapOf() private var isLayoutEnqueued = false var items: MutableList? = null var onTabSelectedListener: ((key: String) -> Unit)? = null var onTabLongPressedListener: ((key: String) -> Unit)? = null + var onNativeLayoutListener: ((width: Double, height: Double) -> Unit)? = null private var activeTintColor: Int? = null private var inactiveTintColor: Int? = null private val checkedStateSet = intArrayOf(android.R.attr.state_checked) @@ -42,6 +51,50 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context private var fontFamily: String? = null private var fontWeight: Int? = null + init { + val layoutHolderFrameLayout = LayoutParams( + LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT + ) + addView(layoutHolder, layoutHolderFrameLayout) + + val bottomNavParams = LayoutParams( + LayoutParams.MATCH_PARENT, + LayoutParams.WRAP_CONTENT + ).apply { + gravity = Gravity.BOTTOM + } + + addView(bottomNavigation, bottomNavParams) + + post { + this.addOnLayoutChangeListener { _, left, top, right, bottom, + oldLeft, oldTop, oldRight, oldBottom -> + val newWidth = right - left + val newHeight = bottom - top + val oldWidth = oldRight - oldLeft + val oldHeight = oldBottom - oldTop + if (newWidth != oldWidth || newHeight != oldHeight) { + val availableHeight = height - bottomNavigation.height + val displayDensity = context.resources.displayMetrics.density + + val dpWidth = (width / displayDensity).toDouble() + val dpHeight = (availableHeight / displayDensity).toDouble() + + onNativeLayoutListener?.invoke(dpWidth, dpHeight) + } + } + } + } + + override fun addView(child: View, index: Int, params: ViewGroup.LayoutParams?) { + if (child === layoutHolder || child === bottomNavigation) { + super.addView(child, index, params) + } else { + layoutHolder.addView(child, params) + } + } + private val imageLoader = ImageLoader.Builder(context) .components { add(SvgDecoder.Factory()) @@ -105,11 +158,11 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context } if (item.badge.isNotEmpty()) { - val badge = this.getOrCreateBadge(index) + val badge = bottomNavigation.getOrCreateBadge(index) badge.isVisible = true badge.text = item.badge } else { - removeBadge(index) + bottomNavigation.removeBadge(index) } post { val itemView = findViewById(menuItem.itemId) @@ -136,7 +189,7 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context } private fun getOrCreateItem(index: Int, title: String): MenuItem { - return menu.findItem(index) ?: menu.add(0, index, 0, title) + return bottomNavigation.menu.findItem(index) ?: bottomNavigation.menu.add(0, index, 0, title) } fun setIcons(icons: ReadableArray?) { @@ -155,7 +208,7 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context this.iconSources[idx] = imageSource // Update existing item if exists. - menu.findItem(idx)?.let { menuItem -> + bottomNavigation.menu.findItem(idx)?.let { menuItem -> getDrawable(imageSource) { menuItem.icon = it } @@ -164,7 +217,7 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context } fun setLabeled(labeled: Boolean?) { - labelVisibilityMode = when (labeled) { + bottomNavigation.labelVisibilityMode = when (labeled) { false -> { LABEL_VISIBILITY_UNLABELED } @@ -178,7 +231,7 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context } fun setRippleColor(color: ColorStateList) { - itemRippleColor = color + bottomNavigation.itemRippleColor = color } @SuppressLint("CheckResult") @@ -205,7 +258,7 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context // Apply the same color to both active and inactive states val colorDrawable = ColorDrawable(backgroundColor) - itemBackground = colorDrawable + bottomNavigation.itemBackground = colorDrawable backgroundTintList = ColorStateList.valueOf(backgroundColor) } @@ -220,7 +273,7 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context } fun setActiveIndicatorColor(color: ColorStateList) { - itemActiveIndicatorColor = color + bottomNavigation.itemActiveIndicatorColor = color } fun setHapticFeedback(enabled: Boolean) { @@ -248,6 +301,10 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context else -> Typeface.NORMAL } + fun setSelectedItemId(itemId: Int) { + bottomNavigation.selectedItemId = itemId + } + private fun updateTextAppearance() { if (fontSize != null || fontFamily != null || fontWeight != null) { val menuView = getChildAt(0) as? ViewGroup ?: return @@ -293,8 +350,8 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context val colors = intArrayOf(colorSecondary, colorPrimary) ColorStateList(states, colors).apply { - this@ReactBottomNavigationView.itemTextColor = this - this@ReactBottomNavigationView.itemIconTintList = this + this@ReactBottomNavigationView.bottomNavigation.itemTextColor = this + this@ReactBottomNavigationView.bottomNavigation.itemIconTintList = this } } diff --git a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt index 3d51039f..5149932a 100644 --- a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt +++ b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt @@ -1,12 +1,11 @@ package com.rcttabview import android.content.res.ColorStateList -import android.graphics.Color -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import com.facebook.react.bridge.ReactContext import com.facebook.react.bridge.ReadableArray import com.facebook.react.common.MapBuilder +import com.rcttabview.events.OnNativeLayoutEvent +import com.rcttabview.events.PageSelectedEvent +import com.rcttabview.events.TabLongPressEvent data class TabInfo( val key: String, @@ -43,7 +42,7 @@ class RCTTabViewImpl { fun setSelectedPage(view: ReactBottomNavigationView, key: String) { view.items?.indexOfFirst { it.key == key }?.let { - view.selectedItemId = it + view.setSelectedItemId(it) } } @@ -90,32 +89,13 @@ class RCTTabViewImpl { PageSelectedEvent.EVENT_NAME, MapBuilder.of("registrationName", "onPageSelected"), TabLongPressEvent.EVENT_NAME, - MapBuilder.of("registrationName", "onTabLongPress") + MapBuilder.of("registrationName", "onTabLongPress"), + OnNativeLayoutEvent.EVENT_NAME, + MapBuilder.of("registrationName", "onNativeLayout") ) } companion object { const val NAME = "RNCTabView" - - // Detect `react-native-edge-to-edge` (https://github.com/zoontek/react-native-edge-to-edge) - private val EDGE_TO_EDGE = try { - Class.forName("com.zoontek.rnedgetoedge.EdgeToEdgePackage") - true - } catch (exception: ClassNotFoundException) { - false - } - - fun getNavigationBarInset(context: ReactContext): Int { - val window = context.currentActivity?.window - - val isSystemBarTransparent = EDGE_TO_EDGE || window?.navigationBarColor == Color.TRANSPARENT - - if (!isSystemBarTransparent) { - return 0 - } - - val windowInsets = ViewCompat.getRootWindowInsets(window?.decorView ?: return 0) - return windowInsets?.getInsets(WindowInsetsCompat.Type.navigationBars())?.bottom ?: 0 - } } } diff --git a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/events/OnNativeLayoutEvent.kt b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/events/OnNativeLayoutEvent.kt new file mode 100644 index 00000000..8391ac8a --- /dev/null +++ b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/events/OnNativeLayoutEvent.kt @@ -0,0 +1,25 @@ +package com.rcttabview.events + +import com.facebook.react.bridge.Arguments +import com.facebook.react.uimanager.events.Event +import com.facebook.react.uimanager.events.RCTEventEmitter + +class OnNativeLayoutEvent(viewTag: Int, private val width: Double, private val height: Double) : + Event(viewTag) { + + companion object { + const val EVENT_NAME = "onNativeLayout" + } + + override fun getEventName(): String { + return EVENT_NAME + } + + override fun dispatch(rctEventEmitter: RCTEventEmitter) { + val event = Arguments.createMap().apply { + putDouble("width", width) + putDouble("height", height) + } + rctEventEmitter.receiveEvent(viewTag, eventName, event) + } +} diff --git a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/PageSelectedEvent.kt b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/events/PageSelectedEvent.kt similarity index 96% rename from packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/PageSelectedEvent.kt rename to packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/events/PageSelectedEvent.kt index 1ba97cb7..ca63ced2 100644 --- a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/PageSelectedEvent.kt +++ b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/events/PageSelectedEvent.kt @@ -1,4 +1,4 @@ -package com.rcttabview +package com.rcttabview.events import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.WritableMap diff --git a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/TabLongPressedEvent.kt b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/events/TabLongPressedEvent.kt similarity index 95% rename from packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/TabLongPressedEvent.kt rename to packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/events/TabLongPressedEvent.kt index 2e6186fe..bd4d8d47 100644 --- a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/TabLongPressedEvent.kt +++ b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/events/TabLongPressedEvent.kt @@ -1,4 +1,4 @@ -package com.rcttabview +package com.rcttabview.events import com.facebook.react.bridge.Arguments import com.facebook.react.uimanager.events.Event diff --git a/packages/react-native-bottom-tabs/android/src/oldarch/RCTTabViewManager.kt b/packages/react-native-bottom-tabs/android/src/oldarch/RCTTabViewManager.kt index 8fce7a3d..eeabbcb0 100644 --- a/packages/react-native-bottom-tabs/android/src/oldarch/RCTTabViewManager.kt +++ b/packages/react-native-bottom-tabs/android/src/oldarch/RCTTabViewManager.kt @@ -1,22 +1,19 @@ package com.rcttabview -import android.view.View.MeasureSpec -import com.facebook.react.bridge.ReadableArray import com.facebook.react.module.annotations.ReactModule -import com.facebook.react.uimanager.LayoutShadowNode -import com.facebook.react.uimanager.SimpleViewManager import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.annotations.ReactProp import com.facebook.react.uimanager.events.EventDispatcher -import com.facebook.yoga.YogaMeasureFunction -import com.facebook.yoga.YogaMeasureMode -import com.facebook.yoga.YogaMeasureOutput -import com.facebook.yoga.YogaNode import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReadableArray import com.facebook.react.uimanager.UIManagerModule +import com.facebook.react.uimanager.ViewGroupManager +import com.rcttabview.events.OnNativeLayoutEvent +import com.rcttabview.events.PageSelectedEvent +import com.rcttabview.events.TabLongPressEvent @ReactModule(name = RCTTabViewImpl.NAME) -class RCTTabViewManager(context: ReactApplicationContext) : SimpleViewManager() { +class RCTTabViewManager(context: ReactApplicationContext) : ViewGroupManager() { private lateinit var eventDispatcher: EventDispatcher private var tabViewImpl = RCTTabViewImpl() @@ -34,11 +31,11 @@ class RCTTabViewManager(context: ReactApplicationContext) : SimpleViewManager eventDispatcher.dispatchEvent(TabLongPressEvent(viewTag = view.id, key)) } - return view - } - override fun createShadowNodeInstance(): LayoutShadowNode { - return TabViewShadowNode() + view.onNativeLayoutListener = { width, height -> + eventDispatcher.dispatchEvent(OnNativeLayoutEvent(viewTag = view.id, width, height)) + } + return view } override fun getExportedCustomDirectEventTypeConstants(): MutableMap? { @@ -124,42 +121,4 @@ class RCTTabViewManager(context: ReactApplicationContext) : SimpleViewManager({ setTabBarHeight(height); }} onNativeLayout={({ nativeEvent: { width, height } }) => { + console.log('onNativeLayout', width, height); setMeasuredDimensions({ width, height }); }} hapticFeedbackEnabled={hapticFeedbackEnabled} @@ -326,7 +327,7 @@ const TabView = ({ } style={ Platform.OS === 'android' - ? [StyleSheet.absoluteFill, { zIndex, opacity }] + ? [measuredDimensions, { zIndex, opacity }] : [{ position: 'absolute' }, measuredDimensions] } > diff --git a/packages/react-native-bottom-tabs/src/TabViewAdapter.android.tsx b/packages/react-native-bottom-tabs/src/TabViewAdapter.android.tsx index de739f17..42e1efb3 100644 --- a/packages/react-native-bottom-tabs/src/TabViewAdapter.android.tsx +++ b/packages/react-native-bottom-tabs/src/TabViewAdapter.android.tsx @@ -4,23 +4,14 @@ import { StyleSheet, View } from 'react-native'; const TabViewAdapter = ({ children, style: _, ...props }: TabViewProps) => { return ( - <> - {children} - - + + {children} + ); }; const styles = StyleSheet.create({ - container: { - width: '100%', - height: '100%', - }, - content: { - flex: 1, - }, - tabContent: { - flex: 1, + full: { width: '100%', height: '100%', }, From 88a4efd5ab33e33f9d27b00dfa96430065a43e88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Mon, 13 Jan 2025 15:19:30 +0100 Subject: [PATCH 2/9] feat: use ViewPager --- apps/example/src/Examples/ThreeTabs.tsx | 2 +- .../main/java/com/rcttabview/RCTTabView.kt | 200 ++++++++++-------- .../java/com/rcttabview/RCTTabViewImpl.kt | 9 +- .../src/main/java/com/rcttabview/Utils.kt | 31 +++ .../java/com/rcttabview/ViewPagerAdapter.kt | 77 +++++++ .../android/src/oldarch/RCTTabViewManager.kt | 32 +++ .../react-native-bottom-tabs/src/TabView.tsx | 8 +- 7 files changed, 254 insertions(+), 105 deletions(-) create mode 100644 packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/Utils.kt create mode 100644 packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/ViewPagerAdapter.kt diff --git a/apps/example/src/Examples/ThreeTabs.tsx b/apps/example/src/Examples/ThreeTabs.tsx index e5b44634..12c7b081 100644 --- a/apps/example/src/Examples/ThreeTabs.tsx +++ b/apps/example/src/Examples/ThreeTabs.tsx @@ -5,7 +5,7 @@ import { Albums } from '../Screens/Albums'; import { Contacts } from '../Screens/Contacts'; export default function ThreeTabs() { - const [index, setIndex] = useState(0); + const [index, setIndex] = useState(1); const [routes] = useState([ { key: 'article', diff --git a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt index 95161ea0..6d5a3239 100644 --- a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt +++ b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt @@ -1,14 +1,14 @@ package com.rcttabview import android.annotation.SuppressLint -import android.content.Context import android.content.res.ColorStateList import android.graphics.Color -import android.graphics.Typeface import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.os.Build +import android.transition.TransitionManager import android.util.Log +import android.util.Size import android.util.TypedValue import android.view.Choreographer import android.view.Gravity @@ -18,30 +18,36 @@ import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import android.widget.TextView -import androidx.appcompat.content.res.AppCompatResources +import androidx.viewpager2.widget.ViewPager2 import coil3.ImageLoader import coil3.asDrawable +import coil3.request.ImageRequest +import coil3.svg.SvgDecoder +import com.facebook.react.bridge.ReactContext import com.facebook.react.bridge.ReadableArray import com.facebook.react.common.assets.ReactFontManager import com.facebook.react.modules.core.ReactChoreographer import com.facebook.react.views.text.ReactTypefaceUtils import com.google.android.material.bottomnavigation.BottomNavigationView -import coil3.request.ImageRequest -import coil3.svg.SvgDecoder import com.google.android.material.navigation.NavigationBarView.LABEL_VISIBILITY_AUTO import com.google.android.material.navigation.NavigationBarView.LABEL_VISIBILITY_LABELED import com.google.android.material.navigation.NavigationBarView.LABEL_VISIBILITY_UNLABELED +import com.google.android.material.transition.platform.MaterialFadeThrough - -class ReactBottomNavigationView(context: Context) : FrameLayout(context) { +class ReactBottomNavigationView(context: ReactContext) : FrameLayout(context) { + private val reactContext: ReactContext = context private val bottomNavigation = BottomNavigationView(context) - private val layoutHolder = FrameLayout(context) - private val iconSources: MutableMap = mutableMapOf() - private var isLayoutEnqueued = false - var items: MutableList? = null + private val viewPager = ViewPager2(context) + val viewPagerAdapter = ViewPagerAdapter() + var onTabSelectedListener: ((key: String) -> Unit)? = null var onTabLongPressedListener: ((key: String) -> Unit)? = null var onNativeLayoutListener: ((width: Double, height: Double) -> Unit)? = null + var disablePageTransitions = false + var items: MutableList? = null + + private var selectedItem: String? = null + private val iconSources: MutableMap = mutableMapOf() private var activeTintColor: Int? = null private var inactiveTintColor: Int? = null private val checkedStateSet = intArrayOf(android.R.attr.state_checked) @@ -50,59 +56,74 @@ class ReactBottomNavigationView(context: Context) : FrameLayout(context) { private var fontSize: Int? = null private var fontFamily: String? = null private var fontWeight: Int? = null + private var lastReportedSize: Size? = null + + private val imageLoader = ImageLoader.Builder(context) + .components { + add(SvgDecoder.Factory()) + } + .build() init { - val layoutHolderFrameLayout = LayoutParams( - LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT + viewPager.adapter = viewPagerAdapter + viewPager.isUserInputEnabled = false + + viewPager.id = View.generateViewId() + addView( + viewPager, LayoutParams( + LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT + ) ) - addView(layoutHolder, layoutHolderFrameLayout) - val bottomNavParams = LayoutParams( + addView(bottomNavigation, LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT ).apply { gravity = Gravity.BOTTOM - } - - addView(bottomNavigation, bottomNavParams) + }) post { - this.addOnLayoutChangeListener { _, left, top, right, bottom, - oldLeft, oldTop, oldRight, oldBottom -> + addOnLayoutChangeListener { _, left, top, right, bottom, + _, _, _, _ -> val newWidth = right - left val newHeight = bottom - top - val oldWidth = oldRight - oldLeft - val oldHeight = oldBottom - oldTop - if (newWidth != oldWidth || newHeight != oldHeight) { - val availableHeight = height - bottomNavigation.height - val displayDensity = context.resources.displayMetrics.density - val dpWidth = (width / displayDensity).toDouble() - val dpHeight = (availableHeight / displayDensity).toDouble() + if (newWidth != lastReportedSize?.width || newHeight != lastReportedSize?.height) { + // We subtract bottom navigation height from the screen height + // This should be refactored for adaptive navigation + val availableHeight = viewPager.height - bottomNavigation.height + + val dpWidth = Utils.convertPixelsToDp(context, viewPager.width) + val dpHeight = Utils.convertPixelsToDp(context, availableHeight) onNativeLayoutListener?.invoke(dpWidth, dpHeight) + lastReportedSize = Size(newWidth, newHeight) } } } } + fun setSelectedItem(value: String) { + selectedItem = value + items?.indexOfFirst { it.key == value }?.let { + setSelectedIndex(it) + } + } + override fun addView(child: View, index: Int, params: ViewGroup.LayoutParams?) { - if (child === layoutHolder || child === bottomNavigation) { + if (child === viewPager || child === bottomNavigation) { super.addView(child, index, params) } else { - layoutHolder.addView(child, params) + viewPagerAdapter.addChild(child, index) + val itemKey = items?.get(index)?.key + if (selectedItem == itemKey) { + setSelectedIndex(index) + } } } - private val imageLoader = ImageLoader.Builder(context) - .components { - add(SvgDecoder.Factory()) - } - .build() - private val layoutCallback = Choreographer.FrameCallback { - isLayoutEnqueued = false measure( MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY), @@ -110,20 +131,11 @@ class ReactBottomNavigationView(context: Context) : FrameLayout(context) { layout(left, top, right, bottom) } - private fun onTabLongPressed(item: MenuItem) { - val longPressedItem = items?.firstOrNull { it.title == item.title } - longPressedItem?.let { - onTabLongPressedListener?.invoke(longPressedItem.key) - emitHapticFeedback(HapticFeedbackConstants.LONG_PRESS) - } - } - override fun requestLayout() { super.requestLayout() @Suppress("SENSELESS_COMPARISON") // layoutCallback can be null here since this method can be called in init - if (!isLayoutEnqueued && layoutCallback != null) { - isLayoutEnqueued = true + if (layoutCallback != null) { // we use NATIVE_ANIMATED_MODULE choreographer queue because it allows us to catch the current // looper loop instead of enqueueing the update in the next loop causing a one frame delay. ReactChoreographer @@ -135,10 +147,16 @@ class ReactBottomNavigationView(context: Context) : FrameLayout(context) { } } - private fun onTabSelected(item: MenuItem) { - if (isLayoutEnqueued) { - return + private fun setSelectedIndex(itemId: Int) { + bottomNavigation.selectedItemId = itemId + if (!disablePageTransitions) { + val fadeThrough = MaterialFadeThrough() + TransitionManager.beginDelayedTransition(this, fadeThrough) } + viewPager.setCurrentItem(itemId, false) + } + + private fun onTabSelected(item: MenuItem) { val selectedItem = items?.first { it.title == item.title } selectedItem?.let { onTabSelectedListener?.invoke(selectedItem.key) @@ -146,13 +164,21 @@ class ReactBottomNavigationView(context: Context) : FrameLayout(context) { } } + private fun onTabLongPressed(item: MenuItem) { + val longPressedItem = items?.firstOrNull { it.title == item.title } + longPressedItem?.let { + onTabLongPressedListener?.invoke(longPressedItem.key) + emitHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + } + } + fun updateItems(items: MutableList) { this.items = items items.forEachIndexed { index, item -> val menuItem = getOrCreateItem(index, item.title) menuItem.isVisible = !item.hidden if (iconSources.containsKey(index)) { - getDrawable(iconSources[index]!!) { + getDrawable(iconSources[index]!!) { menuItem.icon = it } } @@ -165,7 +191,7 @@ class ReactBottomNavigationView(context: Context) : FrameLayout(context) { bottomNavigation.removeBadge(index) } post { - val itemView = findViewById(menuItem.itemId) + val itemView = bottomNavigation.findViewById(menuItem.itemId) itemView?.let { view -> view.setOnLongClickListener { onTabLongPressed(menuItem) @@ -177,9 +203,10 @@ class ReactBottomNavigationView(context: Context) : FrameLayout(context) { } item.testID?.let { testId -> - view.findViewById(com.google.android.material.R.id.navigation_bar_item_content_container)?.apply { + view.findViewById(com.google.android.material.R.id.navigation_bar_item_content_container) + ?.apply { tag = testId - } + } } } updateTextAppearance() @@ -209,7 +236,7 @@ class ReactBottomNavigationView(context: Context) : FrameLayout(context) { // Update existing item if exists. bottomNavigation.menu.findItem(idx)?.let { menuItem -> - getDrawable(imageSource) { + getDrawable(imageSource) { menuItem.icon = it } } @@ -218,15 +245,17 @@ class ReactBottomNavigationView(context: Context) : FrameLayout(context) { fun setLabeled(labeled: Boolean?) { bottomNavigation.labelVisibilityMode = when (labeled) { - false -> { - LABEL_VISIBILITY_UNLABELED - } - true -> { - LABEL_VISIBILITY_LABELED - } - else -> { - LABEL_VISIBILITY_AUTO - } + false -> { + LABEL_VISIBILITY_UNLABELED + } + + true -> { + LABEL_VISIBILITY_LABELED + } + + else -> { + LABEL_VISIBILITY_AUTO + } } } @@ -253,13 +282,16 @@ class ReactBottomNavigationView(context: Context) : FrameLayout(context) { fun setBarTintColor(color: Int?) { // Set the color, either using the active background color or a default color. - val backgroundColor = color ?: getDefaultColorFor(android.R.attr.colorPrimary) ?: return + val backgroundColor = + color ?: Utils.getDefaultColorFor(context, android.R.attr.colorPrimary) ?: return // Apply the same color to both active and inactive states val colorDrawable = ColorDrawable(backgroundColor) bottomNavigation.itemBackground = colorDrawable backgroundTintList = ColorStateList.valueOf(backgroundColor) + // Set navigationBarColor for edge-to-edge. + reactContext.currentActivity?.window?.navigationBarColor = backgroundColor } fun setActiveTintColor(color: Int?) { @@ -276,10 +308,6 @@ class ReactBottomNavigationView(context: Context) : FrameLayout(context) { bottomNavigation.itemActiveIndicatorColor = color } - fun setHapticFeedback(enabled: Boolean) { - hapticFeedbackEnabled = enabled - } - fun setFontSize(size: Int) { fontSize = size updateTextAppearance() @@ -296,22 +324,13 @@ class ReactBottomNavigationView(context: Context) : FrameLayout(context) { updateTextAppearance() } - private fun getTypefaceStyle(weight: Int?) = when (weight) { - 700 -> Typeface.BOLD - else -> Typeface.NORMAL - } - - fun setSelectedItemId(itemId: Int) { - bottomNavigation.selectedItemId = itemId - } - private fun updateTextAppearance() { if (fontSize != null || fontFamily != null || fontWeight != null) { val menuView = getChildAt(0) as? ViewGroup ?: return val size = fontSize?.toFloat()?.takeIf { it > 0 } ?: 12f val typeface = ReactFontManager.getInstance().getTypeface( fontFamily ?: "", - getTypefaceStyle(fontWeight), + Utils.getTypefaceStyle(fontWeight), context.assets ) @@ -343,9 +362,13 @@ class ReactBottomNavigationView(context: Context) : FrameLayout(context) { val currentItemTintColor = items?.find { it.title == item?.title }?.activeTintColor // getDefaultColor will always return a valid color but to satisfy the compiler we need to check for null - val colorPrimary = currentItemTintColor ?: activeTintColor ?: getDefaultColorFor(android.R.attr.colorPrimary) ?: return + val colorPrimary = currentItemTintColor ?: activeTintColor ?: Utils.getDefaultColorFor( + context, + android.R.attr.colorPrimary + ) ?: return val colorSecondary = - inactiveTintColor ?: getDefaultColorFor(android.R.attr.textColorSecondary) ?: return + inactiveTintColor ?: Utils.getDefaultColorFor(context, android.R.attr.textColorSecondary) + ?: return val states = arrayOf(uncheckedStateSet, checkedStateSet) val colors = intArrayOf(colorSecondary, colorPrimary) @@ -355,17 +378,8 @@ class ReactBottomNavigationView(context: Context) : FrameLayout(context) { } } - private fun getDefaultColorFor(baseColorThemeAttr: Int): Int? { - val value = TypedValue() - if (!context.theme.resolveAttribute(baseColorThemeAttr, value, true)) { - return null - } - val baseColor = AppCompatResources.getColorStateList( - context, value.resourceId - ) - return baseColor.defaultColor + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + reactContext.currentActivity?.window?.navigationBarColor = Color.TRANSPARENT } } - - - diff --git a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt index 5149932a..2358ec98 100644 --- a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt +++ b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt @@ -41,9 +41,10 @@ class RCTTabViewImpl { } fun setSelectedPage(view: ReactBottomNavigationView, key: String) { - view.items?.indexOfFirst { it.key == key }?.let { - view.setSelectedItemId(it) - } + view.setSelectedItem(key) +// view.items?.indexOfFirst { it.key == key }?.let { +// view.setSelectedItemId(it) +// } } fun setLabeled(view: ReactBottomNavigationView, flag: Boolean?) { @@ -81,7 +82,7 @@ class RCTTabViewImpl { } fun setHapticFeedbackEnabled(view: ReactBottomNavigationView, enabled: Boolean) { - view.setHapticFeedback(enabled) + view.isHapticFeedbackEnabled = enabled } fun getExportedCustomDirectEventTypeConstants(): MutableMap? { diff --git a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/Utils.kt b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/Utils.kt new file mode 100644 index 00000000..52144e09 --- /dev/null +++ b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/Utils.kt @@ -0,0 +1,31 @@ +package com.rcttabview + +import android.content.Context +import android.graphics.Typeface +import android.util.TypedValue +import androidx.appcompat.content.res.AppCompatResources + +class Utils { + companion object { + fun convertPixelsToDp(context: Context, value: Int): Double { + val displayDensity = context.resources.displayMetrics.density + return (value / displayDensity).toDouble() + } + + fun getTypefaceStyle(weight: Int?) = when (weight) { + 700 -> Typeface.BOLD + else -> Typeface.NORMAL + } + + fun getDefaultColorFor(context: Context, baseColorThemeAttr: Int): Int? { + val value = TypedValue() + if (!context.theme.resolveAttribute(baseColorThemeAttr, value, true)) { + return null + } + val baseColor = AppCompatResources.getColorStateList( + context, value.resourceId + ) + return baseColor.defaultColor + } + } +} diff --git a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/ViewPagerAdapter.kt b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/ViewPagerAdapter.kt new file mode 100644 index 00000000..0541dfb3 --- /dev/null +++ b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/ViewPagerAdapter.kt @@ -0,0 +1,77 @@ +package com.rcttabview + +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.recyclerview.widget.RecyclerView + +class ViewPagerAdapter : RecyclerView.Adapter() { + private val childrenViews: ArrayList = ArrayList() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder(FrameLayout(parent.context).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + }) + } + + override fun onBindViewHolder(holder: ViewHolder, index: Int) { + val container: FrameLayout = holder.container as FrameLayout + val child = getChildAt(index) + holder.setIsRecyclable(false) + + if (container.childCount > 0) { + container.removeAllViews() + } + + if (child.parent != null) { + (child.parent as FrameLayout).removeView(child) + } + + container.addView(child) + } + + override fun getItemCount(): Int { + return childrenViews.size + } + + fun addChild(child: View, index: Int) { + childrenViews.add(index, child) + notifyItemInserted(index) + } + + fun getChildAt(index: Int): View { + return childrenViews[index] + } + + fun removeChild(child: View) { + val index = childrenViews.indexOf(child) + + if(index > -1) { + removeChildAt(index) + } + } + + fun removeAll() { + for (index in 1..childrenViews.size) { + val child = childrenViews[index-1] + if (child.parent?.parent != null) { + (child.parent.parent as ViewGroup).removeView(child.parent as View) + } + } + val removedChildrenCount = childrenViews.size + childrenViews.clear() + notifyItemRangeRemoved(0, removedChildrenCount) + } + + fun removeChildAt(index: Int) { + if (index >= 0 && index < childrenViews.size) { + childrenViews.removeAt(index) + notifyItemRemoved(index) + } + } + + class ViewHolder(val container: View) : RecyclerView.ViewHolder(container) +} diff --git a/packages/react-native-bottom-tabs/android/src/oldarch/RCTTabViewManager.kt b/packages/react-native-bottom-tabs/android/src/oldarch/RCTTabViewManager.kt index eeabbcb0..8dc5e6aa 100644 --- a/packages/react-native-bottom-tabs/android/src/oldarch/RCTTabViewManager.kt +++ b/packages/react-native-bottom-tabs/android/src/oldarch/RCTTabViewManager.kt @@ -1,5 +1,7 @@ package com.rcttabview +import android.view.View +import android.view.ViewGroup import com.facebook.react.module.annotations.ReactModule import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.annotations.ReactProp @@ -38,6 +40,36 @@ class RCTTabViewManager(context: ReactApplicationContext) : ViewGroupManager? { return tabViewImpl.getExportedCustomDirectEventTypeConstants() } diff --git a/packages/react-native-bottom-tabs/src/TabView.tsx b/packages/react-native-bottom-tabs/src/TabView.tsx index d8394984..f5bb9060 100644 --- a/packages/react-native-bottom-tabs/src/TabView.tsx +++ b/packages/react-native-bottom-tabs/src/TabView.tsx @@ -288,7 +288,6 @@ const TabView = ({ setTabBarHeight(height); }} onNativeLayout={({ nativeEvent: { width, height } }) => { - console.log('onNativeLayout', width, height); setMeasuredDimensions({ width, height }); }} hapticFeedbackEnabled={hapticFeedbackEnabled} @@ -300,9 +299,6 @@ const TabView = ({ {trimmedRoutes.map((route) => { if (getLazy({ route }) !== false && !loaded.includes(route.key)) { // Don't render a screen if we've never navigated to it - if (Platform.OS === 'android') { - return null; - } return ( ({ } const focused = route.key === focusedKey; - const opacity = focused ? 1 : 0; - const zIndex = focused ? 0 : -1; return ( ({ } style={ Platform.OS === 'android' - ? [measuredDimensions, { zIndex, opacity }] + ? [measuredDimensions] : [{ position: 'absolute' }, measuredDimensions] } > From 3852ab3272337db009f50efd2dcda24b16a679bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Tue, 14 Jan 2025 10:33:48 +0100 Subject: [PATCH 3/9] fix: remove custom shadow nodes measurements --- apps/example/android/gradle.properties | 2 +- apps/example/src/App.tsx | 6 +- .../Examples/NativeBottomTabsUnmounting.tsx | 63 ++++++++++++++ .../android/build.gradle | 5 -- .../main/java/com/rcttabview/RCTTabView.kt | 31 ++++--- .../java/com/rcttabview/RCTTabViewImpl.kt | 23 +++-- .../src/main/java/com/rcttabview/Utils.kt | 8 ++ .../android/src/main/jni/CMakeLists.txt | 87 ------------------- .../android/src/main/jni/RNCTabView.h | 13 --- .../android/src/newarch/RCTTabViewManager.kt | 82 +++++++++-------- .../RNCTabViewComponentDescriptor.h | 47 ---------- .../RNCTabViewMeasurementsManager.cpp | 65 -------------- .../RNCTabViewMeasurementsManager.h | 31 ------- .../RNCTabView/RNCTabViewShadowNode.cpp | 28 ------ .../RNCTabView/RNCTabViewShadowNode.h | 48 ---------- .../components/RNCTabView/RNCTabViewState.h | 35 -------- .../ios/Fabric/RCTTabViewComponentView.mm | 1 - .../react-native-bottom-tabs/package.json | 2 - .../react-native-bottom-tabs.podspec | 7 -- .../react-native.config.js | 11 --- .../src/TabViewNativeComponent.ts | 4 +- 21 files changed, 150 insertions(+), 449 deletions(-) create mode 100644 apps/example/src/Examples/NativeBottomTabsUnmounting.tsx delete mode 100644 packages/react-native-bottom-tabs/android/src/main/jni/CMakeLists.txt delete mode 100644 packages/react-native-bottom-tabs/android/src/main/jni/RNCTabView.h delete mode 100644 packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewComponentDescriptor.h delete mode 100644 packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewMeasurementsManager.cpp delete mode 100644 packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewMeasurementsManager.h delete mode 100644 packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewShadowNode.cpp delete mode 100644 packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewShadowNode.h delete mode 100644 packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewState.h delete mode 100644 packages/react-native-bottom-tabs/react-native.config.js diff --git a/apps/example/android/gradle.properties b/apps/example/android/gradle.properties index 66ddb865..490e2deb 100644 --- a/apps/example/android/gradle.properties +++ b/apps/example/android/gradle.properties @@ -40,7 +40,7 @@ reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 # to write custom TurboModules/Fabric components OR use libraries that # are providing them. # Note that this is incompatible with web debugging. -newArchEnabled=false +newArchEnabled=true #bridgelessEnabled=true # Uncomment the line below to build React Native from source. diff --git a/apps/example/src/App.tsx b/apps/example/src/App.tsx index f4a42d55..23f5994f 100644 --- a/apps/example/src/App.tsx +++ b/apps/example/src/App.tsx @@ -27,6 +27,7 @@ import TintColorsExample from './Examples/TintColors'; import NativeBottomTabsEmbeddedStacks from './Examples/NativeBottomTabsEmbeddedStacks'; import NativeBottomTabsSVGs from './Examples/NativeBottomTabsSVGs'; import NativeBottomTabsRemoteIcons from './Examples/NativeBottomTabsRemoteIcons'; +import NativeBottomTabsUnmounting from './Examples/NativeBottomTabsUnmounting'; const FourTabsIgnoreSafeArea = () => { return ; @@ -95,7 +96,6 @@ const examples = [ { component: FourTabsNoAnimations, name: 'Four Tabs - no animations', - platform: 'ios', }, { component: FourTabsTransparentScrollEdgeAppearance, @@ -128,6 +128,10 @@ const examples = [ component: NativeBottomTabsSVGs, name: 'Native Bottom Tabs with SVG Icons', }, + { + component: NativeBottomTabsUnmounting, + name: 'Native Bottom Tabs unmounting', + }, { component: NativeBottomTabsRemoteIcons, name: 'Native Bottom Tabs with SVG Remote Icons', diff --git a/apps/example/src/Examples/NativeBottomTabsUnmounting.tsx b/apps/example/src/Examples/NativeBottomTabsUnmounting.tsx new file mode 100644 index 00000000..0d8bc02d --- /dev/null +++ b/apps/example/src/Examples/NativeBottomTabsUnmounting.tsx @@ -0,0 +1,63 @@ +import { Article } from '../Screens/Article'; +import { Albums } from '../Screens/Albums'; +import { Contacts } from '../Screens/Contacts'; +import { Chat } from '../Screens/Chat'; +import { createNativeBottomTabNavigator } from '@bottom-tabs/react-navigation'; +import React from 'react'; +import { Alert } from 'react-native'; + +const Tab = createNativeBottomTabNavigator(); + +function NativeBottomTabsUnmounting() { + const [isTabMounted, setIsTabMounted] = React.useState(true); + + React.useEffect(() => { + const id = setTimeout(() => { + setIsTabMounted(false); + Alert.alert('Tab is unmounted'); + }, 1000); + + return () => clearTimeout(id); + }, []); + return ( + + + focused + ? require('../../assets/icons/person_dark.png') + : require('../../assets/icons/article_dark.png'), + }} + /> + require('../../assets/icons/grid_dark.png'), + }} + /> + require('../../assets/icons/person_dark.png'), + }} + /> + {isTabMounted && ( + require('../../assets/icons/chat_dark.png'), + }} + /> + )} + + ); +} + +export default NativeBottomTabsUnmounting; diff --git a/packages/react-native-bottom-tabs/android/build.gradle b/packages/react-native-bottom-tabs/android/build.gradle index a8fabb4a..a9ac2953 100644 --- a/packages/react-native-bottom-tabs/android/build.gradle +++ b/packages/react-native-bottom-tabs/android/build.gradle @@ -14,11 +14,6 @@ buildscript { } } -def reactNativeArchitectures() { - def value = rootProject.getProperties().get("reactNativeArchitectures") - return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"] -} - def isNewArchitectureEnabled() { return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true" } diff --git a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt index 6d5a3239..c1579972 100644 --- a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt +++ b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt @@ -44,7 +44,7 @@ class ReactBottomNavigationView(context: ReactContext) : FrameLayout(context) { var onTabLongPressedListener: ((key: String) -> Unit)? = null var onNativeLayoutListener: ((width: Double, height: Double) -> Unit)? = null var disablePageTransitions = false - var items: MutableList? = null + var items: MutableList = mutableListOf() private var selectedItem: String? = null private val iconSources: MutableMap = mutableMapOf() @@ -106,9 +106,7 @@ class ReactBottomNavigationView(context: ReactContext) : FrameLayout(context) { fun setSelectedItem(value: String) { selectedItem = value - items?.indexOfFirst { it.key == value }?.let { - setSelectedIndex(it) - } + setSelectedIndex(items.indexOfFirst { it.key == value }) } override fun addView(child: View, index: Int, params: ViewGroup.LayoutParams?) { @@ -116,7 +114,7 @@ class ReactBottomNavigationView(context: ReactContext) : FrameLayout(context) { super.addView(child, index, params) } else { viewPagerAdapter.addChild(child, index) - val itemKey = items?.get(index)?.key + val itemKey = items[index].key if (selectedItem == itemKey) { setSelectedIndex(index) } @@ -157,15 +155,15 @@ class ReactBottomNavigationView(context: ReactContext) : FrameLayout(context) { } private fun onTabSelected(item: MenuItem) { - val selectedItem = items?.first { it.title == item.title } - selectedItem?.let { + val selectedItem = items.first { it.title == item.title } + selectedItem.let { onTabSelectedListener?.invoke(selectedItem.key) emitHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK) } } private fun onTabLongPressed(item: MenuItem) { - val longPressedItem = items?.firstOrNull { it.title == item.title } + val longPressedItem = items.firstOrNull { it.title == item.title } longPressedItem?.let { onTabLongPressedListener?.invoke(longPressedItem.key) emitHapticFeedback(HapticFeedbackConstants.LONG_PRESS) @@ -173,6 +171,10 @@ class ReactBottomNavigationView(context: ReactContext) : FrameLayout(context) { } fun updateItems(items: MutableList) { + // If an item got removed, let's re-add all items + if (items.size < this.items.size) { + bottomNavigation.menu.clear() + } this.items = items items.forEachIndexed { index, item -> val menuItem = getOrCreateItem(index, item.title) @@ -248,11 +250,9 @@ class ReactBottomNavigationView(context: ReactContext) : FrameLayout(context) { false -> { LABEL_VISIBILITY_UNLABELED } - true -> { LABEL_VISIBILITY_LABELED } - else -> { LABEL_VISIBILITY_AUTO } @@ -291,7 +291,9 @@ class ReactBottomNavigationView(context: ReactContext) : FrameLayout(context) { bottomNavigation.itemBackground = colorDrawable backgroundTintList = ColorStateList.valueOf(backgroundColor) // Set navigationBarColor for edge-to-edge. - reactContext.currentActivity?.window?.navigationBarColor = backgroundColor + if (Utils.isEdgeToEdge()) { + reactContext.currentActivity?.window?.navigationBarColor = backgroundColor + } } fun setActiveTintColor(color: Int?) { @@ -359,7 +361,7 @@ class ReactBottomNavigationView(context: ReactContext) : FrameLayout(context) { private fun updateTintColors(item: MenuItem? = null) { // First let's check current item color. - val currentItemTintColor = items?.find { it.title == item?.title }?.activeTintColor + val currentItemTintColor = items.find { it.title == item?.title }?.activeTintColor // getDefaultColor will always return a valid color but to satisfy the compiler we need to check for null val colorPrimary = currentItemTintColor ?: activeTintColor ?: Utils.getDefaultColorFor( @@ -380,6 +382,9 @@ class ReactBottomNavigationView(context: ReactContext) : FrameLayout(context) { override fun onDetachedFromWindow() { super.onDetachedFromWindow() - reactContext.currentActivity?.window?.navigationBarColor = Color.TRANSPARENT + if (Utils.isEdgeToEdge()) { + reactContext.currentActivity?.window?.navigationBarColor = Color.TRANSPARENT + } + imageLoader.shutdown() } } diff --git a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt index 2358ec98..73a02f35 100644 --- a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt +++ b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt @@ -24,17 +24,17 @@ class RCTTabViewImpl { fun setItems(view: ReactBottomNavigationView, items: ReadableArray) { val itemsArray = mutableListOf() for (i in 0 until items.size()) { - items.getMap(i).let { item -> - itemsArray.add( - TabInfo( - key = item.getString("key") ?: "", - title = item.getString("title") ?: "", - badge = item.getString("badge") ?: "", - activeTintColor = if (item.hasKey("activeTintColor")) item.getInt("activeTintColor") else null, - hidden = if (item.hasKey("hidden")) item.getBoolean("hidden") else false, - testID = item.getString("testID") + items.getMap(i)?.let { item -> + itemsArray.add( + TabInfo( + key = item.getString("key") ?: "", + title = item.getString("title") ?: "", + badge = item.getString("badge") ?: "", + activeTintColor = if (item.hasKey("activeTintColor")) item.getInt("activeTintColor") else null, + hidden = if (item.hasKey("hidden")) item.getBoolean("hidden") else false, + testID = item.getString("testID") + ) ) - ) } } view.updateItems(itemsArray) @@ -42,9 +42,6 @@ class RCTTabViewImpl { fun setSelectedPage(view: ReactBottomNavigationView, key: String) { view.setSelectedItem(key) -// view.items?.indexOfFirst { it.key == key }?.let { -// view.setSelectedItemId(it) -// } } fun setLabeled(view: ReactBottomNavigationView, flag: Boolean?) { diff --git a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/Utils.kt b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/Utils.kt index 52144e09..e7c9f4b5 100644 --- a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/Utils.kt +++ b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/Utils.kt @@ -27,5 +27,13 @@ class Utils { ) return baseColor.defaultColor } + + // Detect `react-native-edge-to-edge` (https://github.com/zoontek/react-native-edge-to-edge) + fun isEdgeToEdge() = try { + Class.forName("com.zoontek.rnedgetoedge.EdgeToEdgePackage") + true + } catch (exception: ClassNotFoundException) { + false + } } } diff --git a/packages/react-native-bottom-tabs/android/src/main/jni/CMakeLists.txt b/packages/react-native-bottom-tabs/android/src/main/jni/CMakeLists.txt deleted file mode 100644 index 71147d64..00000000 --- a/packages/react-native-bottom-tabs/android/src/main/jni/CMakeLists.txt +++ /dev/null @@ -1,87 +0,0 @@ -cmake_minimum_required(VERSION 3.13) -set(CMAKE_VERBOSE_MAKEFILE ON) - -set(LIB_LITERAL RNCTabView) -set(LIB_TARGET_NAME react_codegen_${LIB_LITERAL}) - -set(LIB_ANDROID_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../..) -set(LIB_COMMON_DIR ${LIB_ANDROID_DIR}/../common/cpp) -set(LIB_ANDROID_GENERATED_JNI_DIR ${LIB_ANDROID_DIR}/build/generated/source/codegen/jni) -set(LIB_ANDROID_GENERATED_COMPONENTS_DIR ${LIB_ANDROID_GENERATED_JNI_DIR}/react/renderer/components/${LIB_LITERAL}) - -add_compile_options( - -fexceptions - -frtti - -std=c++20 - -Wall - -Wpedantic - -Wno-gnu-zero-variadic-macro-arguments -) - -file(GLOB LIB_CUSTOM_SRCS CONFIGURE_DEPENDS *.cpp ${LIB_COMMON_DIR}/react/renderer/components/${LIB_LITERAL}/*.cpp) -file(GLOB LIB_CODEGEN_SRCS CONFIGURE_DEPENDS ${LIB_ANDROID_GENERATED_JNI_DIR}/*.cpp ${LIB_ANDROID_GENERATED_COMPONENTS_DIR}/*.cpp) - -add_library( - ${LIB_TARGET_NAME} - SHARED - ${LIB_CUSTOM_SRCS} - ${LIB_CODEGEN_SRCS} -) - -target_include_directories( - ${LIB_TARGET_NAME} - PUBLIC - . - ${LIB_COMMON_DIR} - ${LIB_ANDROID_GENERATED_JNI_DIR} - ${LIB_ANDROID_GENERATED_COMPONENTS_DIR} -) - -# https://github.com/react-native-community/discussions-and-proposals/discussions/816 -# This if-then-else can be removed once this library does not support version below 0.76 -if (REACTNATIVE_MERGED_SO) - target_link_libraries( - ${LIB_TARGET_NAME} - fbjni - jsi - reactnative - ) -else() - target_link_libraries( - ${LIB_TARGET_NAME} - fbjni - folly_runtime - glog - jsi - react_codegen_rncore - react_debug - react_render_componentregistry - react_render_core - react_render_debug - react_render_graphics - react_render_imagemanager - react_render_mapbuffer - react_utils - react_nativemodule_core - rrc_image - turbomodulejsijni - rrc_view - yoga - ) -endif() - -target_compile_options( - ${LIB_TARGET_NAME} - PRIVATE - -DLOG_TAG=\"ReactNative\" - -fexceptions - -frtti - -std=c++20 - -Wall -) - -target_include_directories( - ${CMAKE_PROJECT_NAME} - PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR} -) diff --git a/packages/react-native-bottom-tabs/android/src/main/jni/RNCTabView.h b/packages/react-native-bottom-tabs/android/src/main/jni/RNCTabView.h deleted file mode 100644 index f7a1fb25..00000000 --- a/packages/react-native-bottom-tabs/android/src/main/jni/RNCTabView.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -namespace facebook::react { -JSI_EXPORT -std::shared_ptr RNCTabView_ModuleProvider( - const std::string &moduleName, - const JavaTurboModule::InitParams ¶ms); -} diff --git a/packages/react-native-bottom-tabs/android/src/newarch/RCTTabViewManager.kt b/packages/react-native-bottom-tabs/android/src/newarch/RCTTabViewManager.kt index 0d3935d6..3d5b4fcc 100644 --- a/packages/react-native-bottom-tabs/android/src/newarch/RCTTabViewManager.kt +++ b/packages/react-native-bottom-tabs/android/src/newarch/RCTTabViewManager.kt @@ -1,28 +1,26 @@ package com.rcttabview -import android.content.Context import android.view.View +import android.view.ViewGroup import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReadableArray -import com.facebook.react.bridge.ReadableMap import com.facebook.react.module.annotations.ReactModule -import com.facebook.react.uimanager.PixelUtil.toDIPFromPixel -import com.facebook.react.uimanager.SimpleViewManager import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.UIManagerHelper +import com.facebook.react.uimanager.ViewGroupManager import com.facebook.react.uimanager.ViewManagerDelegate import com.facebook.react.viewmanagers.RNCTabViewManagerDelegate import com.facebook.react.viewmanagers.RNCTabViewManagerInterface -import com.facebook.yoga.YogaMeasureMode -import com.facebook.yoga.YogaMeasureOutput +import com.rcttabview.events.OnNativeLayoutEvent +import com.rcttabview.events.PageSelectedEvent +import com.rcttabview.events.TabLongPressEvent @ReactModule(name = RCTTabViewImpl.NAME) class RCTTabViewManager(context: ReactApplicationContext) : - SimpleViewManager(), + ViewGroupManager(), RNCTabViewManagerInterface { - private val contextInner: ReactApplicationContext = context private val delegate: RNCTabViewManagerDelegate = RNCTabViewManagerDelegate(this) private val tabViewImpl: RCTTabViewImpl = RCTTabViewImpl() @@ -37,6 +35,10 @@ class RCTTabViewManager(context: ReactApplicationContext) : view.onTabLongPressedListener = { key -> eventDispatcher?.dispatchEvent(TabLongPressEvent(viewTag = view.id, key)) } + + view.onNativeLayoutListener = { width, height -> + eventDispatcher?.dispatchEvent(OnNativeLayoutEvent(viewTag = view.id, width, height)) + } return view } @@ -45,6 +47,36 @@ class RCTTabViewManager(context: ReactApplicationContext) : return tabViewImpl.getName() } + override fun getChildCount(parent: ReactBottomNavigationView): Int { + return parent.viewPagerAdapter.itemCount ?: 0 + } + + override fun getChildAt(parent: ReactBottomNavigationView, index: Int): View? { + return parent.viewPagerAdapter.getChildAt(index) + } + + override fun removeView(parent: ReactBottomNavigationView, view: View) { + parent.viewPagerAdapter.removeChild(view) + } + + override fun removeAllViews(parent: ReactBottomNavigationView) { + parent.viewPagerAdapter.removeAll() + } + + override fun removeViewAt(parent: ReactBottomNavigationView, index: Int) { + val child = parent.viewPagerAdapter.getChildAt(index) + + if (child.parent != null) { + (child.parent as? ViewGroup)?.removeView(child) + } + + parent.viewPagerAdapter.removeChildAt(index) + } + + override fun needsCustomLayoutForChildren(): Boolean { + return true + } + override fun setItems(view: ReactBottomNavigationView?, value: ReadableArray?) { if (view != null && value != null) tabViewImpl.setItems(view, value) @@ -99,30 +131,6 @@ class RCTTabViewManager(context: ReactApplicationContext) : tabViewImpl.setHapticFeedbackEnabled(view, value) } - public override fun measure( - context: Context?, - localData: ReadableMap?, - props: ReadableMap?, - state: ReadableMap?, - width: Float, - widthMode: YogaMeasureMode?, - height: Float, - heightMode: YogaMeasureMode?, - attachmentsPositions: FloatArray? - ): Long { - val view = ReactBottomNavigationView(context ?: contextInner) - val measureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) - view.measure(measureSpec, measureSpec) - - val bottomInset = RCTTabViewImpl.getNavigationBarInset(contextInner) - - return YogaMeasureOutput.make( - // TabBar should always stretch to the width of the screen. - toDIPFromPixel(width), - toDIPFromPixel(view.measuredHeight.toFloat() + bottomInset) - ) - } - override fun setFontFamily(view: ReactBottomNavigationView?, value: String?) { view?.setFontFamily(value) } @@ -135,22 +143,20 @@ class RCTTabViewManager(context: ReactApplicationContext) : view?.setFontSize(value) } - // iOS Methods + override fun setDisablePageAnimations(view: ReactBottomNavigationView?, value: Boolean) { + view?.disablePageTransitions = value + } + // iOS Methods override fun setTranslucent(view: ReactBottomNavigationView?, value: Boolean) { } override fun setIgnoresTopSafeArea(view: ReactBottomNavigationView?, value: Boolean) { } - override fun setDisablePageAnimations(view: ReactBottomNavigationView?, value: Boolean) { - } - override fun setSidebarAdaptable(view: ReactBottomNavigationView?, value: Boolean) { } override fun setScrollEdgeAppearance(view: ReactBottomNavigationView?, value: String?) { } - - } diff --git a/packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewComponentDescriptor.h b/packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewComponentDescriptor.h deleted file mode 100644 index 9e80b06b..00000000 --- a/packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewComponentDescriptor.h +++ /dev/null @@ -1,47 +0,0 @@ -#ifdef __cplusplus - -#pragma once - -#include -#include - -namespace facebook::react { - -class RNCTabViewComponentDescriptor final : public ConcreteComponentDescriptor -{ -#ifdef ANDROID -public: - RNCTabViewComponentDescriptor(const ComponentDescriptorParameters ¶meters) - : ConcreteComponentDescriptor(parameters), measurementsManager_( - std::make_shared(contextContainer_)) {} - - void adopt(ShadowNode &shadowNode) const override - { - ConcreteComponentDescriptor::adopt(shadowNode); - - auto &rncTabViewShadowNode = - static_cast(shadowNode); - - // `RNCTabViewShadowNode` uses `RNCTabViewMeasurementsManager` to - // provide measurements to Yoga. - rncTabViewShadowNode.setSliderMeasurementsManager( - measurementsManager_); - - // All `RNCTabViewShadowNode`s must have leaf Yoga nodes with properly - // setup measure function. - rncTabViewShadowNode.enableMeasurement(); - } - -private: - const std::shared_ptr measurementsManager_; -#else -public: - RNCTabViewComponentDescriptor(const ComponentDescriptorParameters ¶meters) - : ConcreteComponentDescriptor(parameters) {} -#endif - -}; - -} - -#endif diff --git a/packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewMeasurementsManager.cpp b/packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewMeasurementsManager.cpp deleted file mode 100644 index 996da9f4..00000000 --- a/packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewMeasurementsManager.cpp +++ /dev/null @@ -1,65 +0,0 @@ -#ifdef ANDROID -#include "RNCTabViewMeasurementsManager.h" - -#include -#include -#include - - -using namespace facebook::jni; - -namespace facebook::react -{ -Size RNCTabViewMeasurementsManager::measure( - SurfaceId surfaceId, - LayoutConstraints layoutConstraints) const -{ - { - std::scoped_lock lock(mutex_); - if (hasBeenMeasured_) - { - return cachedMeasurement_; - } - } - - const jni::global_ref& fabricUIManager = - contextContainer_->at>("FabricUIManager"); - - static auto measure = facebook::jni::findClassStatic( - "com/facebook/react/fabric/FabricUIManager") - ->getMethod("measure"); - - auto minimumSize = layoutConstraints.minimumSize; - auto maximumSize = layoutConstraints.maximumSize; - - local_ref componentName = make_jstring("RNCTabView"); - - auto measurement = yogaMeassureToSize(measure( - fabricUIManager, - surfaceId, - componentName.get(), - nullptr, - nullptr, - nullptr, - minimumSize.width, - maximumSize.width, - minimumSize.height, - maximumSize.height)); - - std::scoped_lock lock(mutex_); - cachedMeasurement_ = measurement; - hasBeenMeasured_ = true; - return measurement; -} -} - -#endif diff --git a/packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewMeasurementsManager.h b/packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewMeasurementsManager.h deleted file mode 100644 index 1882ac43..00000000 --- a/packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewMeasurementsManager.h +++ /dev/null @@ -1,31 +0,0 @@ -#ifdef __cplusplus - -#ifdef ANDROID -#pragma once -#include -#include -#include - -namespace facebook::react -{ - - class RNCTabViewMeasurementsManager - { - public: - RNCTabViewMeasurementsManager( - const ContextContainer::Shared &contextContainer) - : contextContainer_(contextContainer) {} - - Size measure(SurfaceId surfaceId, LayoutConstraints layoutConstraints) const; - - private: - const ContextContainer::Shared contextContainer_; - mutable std::mutex mutex_; - mutable bool hasBeenMeasured_ = false; - mutable Size cachedMeasurement_{}; - }; -} - -#endif - -#endif diff --git a/packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewShadowNode.cpp b/packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewShadowNode.cpp deleted file mode 100644 index 47867419..00000000 --- a/packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewShadowNode.cpp +++ /dev/null @@ -1,28 +0,0 @@ -#include "RNCTabViewShadowNode.h" -#include "RNCTabViewMeasurementsManager.h" - -namespace facebook::react { - -extern const char RNCTabViewComponentName[] = "RNCTabView"; - -#ifdef ANDROID -void RNCTabViewShadowNode::setSliderMeasurementsManager( - const std::shared_ptr & - measurementsManager) -{ - ensureUnsealed(); - measurementsManager_ = measurementsManager; -} - -#pragma mark - LayoutableShadowNode - -Size RNCTabViewShadowNode::measureContent( - const LayoutContext & /*layoutContext*/, - const LayoutConstraints &layoutConstraints) const -{ - return measurementsManager_->measure(getSurfaceId(), layoutConstraints); -} - -#endif - -} diff --git a/packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewShadowNode.h b/packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewShadowNode.h deleted file mode 100644 index f725d903..00000000 --- a/packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewShadowNode.h +++ /dev/null @@ -1,48 +0,0 @@ -#ifdef __cplusplus - -#pragma once - -#include -#include -#include -#include -#include - -#include "RNCTabViewMeasurementsManager.h" - -namespace facebook::react { - -JSI_EXPORT extern const char RNCTabViewComponentName[]; - -/* -* `ShadowNode` for component. -*/ -class JSI_EXPORT RNCTabViewShadowNode final -: public ConcreteViewShadowNode< -RNCTabViewComponentName, -RNCTabViewProps, -RNCTabViewEventEmitter, -RNCTabViewState> -{ -public: - using ConcreteViewShadowNode::ConcreteViewShadowNode; - -#ifdef ANDROID - void setSliderMeasurementsManager( - const std::shared_ptr &measurementsManager); - - #pragma mark - LayoutableShadowNode - - Size measureContent( - const LayoutContext &layoutContext, - const LayoutConstraints &layoutConstraints) const override; - -private: - std::shared_ptr measurementsManager_; -#endif - -}; - -} - -#endif diff --git a/packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewState.h b/packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewState.h deleted file mode 100644 index cabea07e..00000000 --- a/packages/react-native-bottom-tabs/common/cpp/react/renderer/components/RNCTabView/RNCTabViewState.h +++ /dev/null @@ -1,35 +0,0 @@ -#ifdef __cplusplus - -#pragma once - -#ifdef ANDROID -#include -#include -#include -#endif - -namespace facebook::react { - -class RNCTabViewState -{ -public: - RNCTabViewState() = default; - -#ifdef ANDROID - RNCTabViewState(RNCTabViewState const &previousState, folly::dynamic data) {}; - - folly::dynamic getDynamic() const - { - return {}; - }; - - MapBuffer getMapBuffer() const - { - return MapBufferBuilder::EMPTY(); - }; -#endif -}; - -} - -#endif diff --git a/packages/react-native-bottom-tabs/ios/Fabric/RCTTabViewComponentView.mm b/packages/react-native-bottom-tabs/ios/Fabric/RCTTabViewComponentView.mm index 424081e2..67ba09b7 100644 --- a/packages/react-native-bottom-tabs/ios/Fabric/RCTTabViewComponentView.mm +++ b/packages/react-native-bottom-tabs/ios/Fabric/RCTTabViewComponentView.mm @@ -2,7 +2,6 @@ #import "RCTTabViewComponentView.h" #import -#import #import #import #import diff --git a/packages/react-native-bottom-tabs/package.json b/packages/react-native-bottom-tabs/package.json index ba086449..70a12990 100644 --- a/packages/react-native-bottom-tabs/package.json +++ b/packages/react-native-bottom-tabs/package.json @@ -24,9 +24,7 @@ "files": [ "src", "lib", - "common", "android", - "react-navigation", "ios", "cpp", "react-native.config.js", diff --git a/packages/react-native-bottom-tabs/react-native-bottom-tabs.podspec b/packages/react-native-bottom-tabs/react-native-bottom-tabs.podspec index cafd8e5e..e1d71432 100644 --- a/packages/react-native-bottom-tabs/react-native-bottom-tabs.podspec +++ b/packages/react-native-bottom-tabs/react-native-bottom-tabs.podspec @@ -22,13 +22,6 @@ Pod::Spec.new do |s| s.source_files = "ios/**/*.{h,m,mm,cpp,swift}" s.static_framework = true - if new_arch_enabled - s.subspec "common" do |ss| - ss.source_files = "common/cpp/**/*.{cpp,h}" - ss.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/common/cpp\"" } - end - end - s.dependency "SwiftUIIntrospect", '~> 1.0' s.dependency 'SDWebImage', '>= 5.19.1' s.dependency 'SDWebImageSVGCoder', '>= 1.7.0' diff --git a/packages/react-native-bottom-tabs/react-native.config.js b/packages/react-native-bottom-tabs/react-native.config.js deleted file mode 100644 index b3ebd7d7..00000000 --- a/packages/react-native-bottom-tabs/react-native.config.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = { - dependency: { - platforms: { - android: { - libraryName: 'RNCTabView', - componentDescriptors: ['RNCTabViewComponentDescriptor'], - cmakeListsPath: 'src/main/jni/CMakeLists.txt', - }, - }, - }, -}; diff --git a/packages/react-native-bottom-tabs/src/TabViewNativeComponent.ts b/packages/react-native-bottom-tabs/src/TabViewNativeComponent.ts index 113f990f..0a818b5b 100644 --- a/packages/react-native-bottom-tabs/src/TabViewNativeComponent.ts +++ b/packages/react-native-bottom-tabs/src/TabViewNativeComponent.ts @@ -57,6 +57,4 @@ export interface TabViewProps extends ViewProps { fontSize?: Int32; } -export default codegenNativeComponent('RNCTabView', { - interfaceOnly: true, -}); +export default codegenNativeComponent('RNCTabView'); From 3e964c0e348ac98da77a61965e7bbad94db8d70f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Wed, 15 Jan 2025 15:15:51 +0100 Subject: [PATCH 4/9] feat: add changesets --- .changeset/smart-tomatoes-serve.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/smart-tomatoes-serve.md diff --git a/.changeset/smart-tomatoes-serve.md b/.changeset/smart-tomatoes-serve.md new file mode 100644 index 00000000..1d708c4f --- /dev/null +++ b/.changeset/smart-tomatoes-serve.md @@ -0,0 +1,5 @@ +--- +'react-native-bottom-tabs': minor +--- + +feat: refactor android, change views on the native side, add page animations From 01a75214f60578532de389505007fb91b593b5e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Wed, 15 Jan 2025 16:00:37 +0100 Subject: [PATCH 5/9] fix: use optional chaining for 0.77 RC --- .../android/src/main/java/com/rcttabview/RCTTabView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt index c1579972..16c34d36 100644 --- a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt +++ b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt @@ -228,7 +228,7 @@ class ReactBottomNavigationView(context: ReactContext) : FrameLayout(context) { for (idx in 0 until icons.size()) { val source = icons.getMap(idx) - val uri = source.getString("uri") + val uri = source?.getString("uri") if (uri.isNullOrEmpty()) { continue } From eb2e36c11b7b1a765afdba936c74285c05910ef4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Wed, 15 Jan 2025 16:59:56 +0100 Subject: [PATCH 6/9] fix: use TabViewImpl to share code across architectures --- .../java/com/rcttabview/RCTTabViewImpl.kt | 32 +++++++++++++++++++ .../android/src/newarch/RCTTabViewManager.kt | 18 ++++------- .../android/src/oldarch/RCTTabViewManager.kt | 18 ++++------- 3 files changed, 44 insertions(+), 24 deletions(-) diff --git a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt index 73a02f35..34091117 100644 --- a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt +++ b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt @@ -1,6 +1,8 @@ package com.rcttabview import android.content.res.ColorStateList +import android.view.View +import android.view.ViewGroup import com.facebook.react.bridge.ReadableArray import com.facebook.react.common.MapBuilder import com.rcttabview.events.OnNativeLayoutEvent @@ -93,6 +95,36 @@ class RCTTabViewImpl { ) } + fun getChildCount(parent: ReactBottomNavigationView): Int { + return parent.viewPagerAdapter.itemCount ?: 0 + } + + fun getChildAt(parent: ReactBottomNavigationView, index: Int): View? { + return parent.viewPagerAdapter.getChildAt(index) + } + + fun removeView(parent: ReactBottomNavigationView, view: View) { + parent.viewPagerAdapter.removeChild(view) + } + + fun removeAllViews(parent: ReactBottomNavigationView) { + parent.viewPagerAdapter.removeAll() + } + + fun removeViewAt(parent: ReactBottomNavigationView, index: Int) { + val child = parent.viewPagerAdapter.getChildAt(index) + + if (child.parent != null) { + (child.parent as? ViewGroup)?.removeView(child) + } + + parent.viewPagerAdapter.removeChildAt(index) + } + + fun needsCustomLayoutForChildren(): Boolean { + return true + } + companion object { const val NAME = "RNCTabView" } diff --git a/packages/react-native-bottom-tabs/android/src/newarch/RCTTabViewManager.kt b/packages/react-native-bottom-tabs/android/src/newarch/RCTTabViewManager.kt index 3d5b4fcc..c6059707 100644 --- a/packages/react-native-bottom-tabs/android/src/newarch/RCTTabViewManager.kt +++ b/packages/react-native-bottom-tabs/android/src/newarch/RCTTabViewManager.kt @@ -48,33 +48,27 @@ class RCTTabViewManager(context: ReactApplicationContext) : } override fun getChildCount(parent: ReactBottomNavigationView): Int { - return parent.viewPagerAdapter.itemCount ?: 0 + return tabViewImpl.getChildCount(parent) } override fun getChildAt(parent: ReactBottomNavigationView, index: Int): View? { - return parent.viewPagerAdapter.getChildAt(index) + return tabViewImpl.getChildAt(parent, index) } override fun removeView(parent: ReactBottomNavigationView, view: View) { - parent.viewPagerAdapter.removeChild(view) + tabViewImpl.removeView(parent, view) } override fun removeAllViews(parent: ReactBottomNavigationView) { - parent.viewPagerAdapter.removeAll() + tabViewImpl.removeAllViews(parent) } override fun removeViewAt(parent: ReactBottomNavigationView, index: Int) { - val child = parent.viewPagerAdapter.getChildAt(index) - - if (child.parent != null) { - (child.parent as? ViewGroup)?.removeView(child) - } - - parent.viewPagerAdapter.removeChildAt(index) + tabViewImpl.removeViewAt(parent, index) } override fun needsCustomLayoutForChildren(): Boolean { - return true + return tabViewImpl.needsCustomLayoutForChildren() } override fun setItems(view: ReactBottomNavigationView?, value: ReadableArray?) { diff --git a/packages/react-native-bottom-tabs/android/src/oldarch/RCTTabViewManager.kt b/packages/react-native-bottom-tabs/android/src/oldarch/RCTTabViewManager.kt index 8dc5e6aa..230416f7 100644 --- a/packages/react-native-bottom-tabs/android/src/oldarch/RCTTabViewManager.kt +++ b/packages/react-native-bottom-tabs/android/src/oldarch/RCTTabViewManager.kt @@ -41,33 +41,27 @@ class RCTTabViewManager(context: ReactApplicationContext) : ViewGroupManager? { From 0644e1df5940562f08c7e65faebc71e5be7993af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Wed, 15 Jan 2025 17:13:10 +0100 Subject: [PATCH 7/9] fix: remove TabViewAdapter as its no longer needed --- .../react-native-bottom-tabs/src/TabView.tsx | 6 +++--- .../src/TabViewAdapter.android.tsx | 20 ------------------- .../src/TabViewAdapter.tsx | 7 ------- 3 files changed, 3 insertions(+), 30 deletions(-) delete mode 100644 packages/react-native-bottom-tabs/src/TabViewAdapter.android.tsx delete mode 100644 packages/react-native-bottom-tabs/src/TabViewAdapter.tsx diff --git a/packages/react-native-bottom-tabs/src/TabView.tsx b/packages/react-native-bottom-tabs/src/TabView.tsx index f5bb9060..570b1631 100644 --- a/packages/react-native-bottom-tabs/src/TabView.tsx +++ b/packages/react-native-bottom-tabs/src/TabView.tsx @@ -12,7 +12,7 @@ import { BottomTabBarHeightContext } from './utils/BottomTabBarHeightContext'; //@ts-ignore import type { ImageSource } from 'react-native/Libraries/Image/ImageSource'; -import TabViewAdapter from './TabViewAdapter'; +import NativeTabView from './TabViewNativeComponent'; import useLatestCallback from 'use-latest-callback'; import type { BaseRoute, NavigationState } from './types'; @@ -270,7 +270,7 @@ const TabView = ({ return ( - ({ ); })} - + ); }; diff --git a/packages/react-native-bottom-tabs/src/TabViewAdapter.android.tsx b/packages/react-native-bottom-tabs/src/TabViewAdapter.android.tsx deleted file mode 100644 index 42e1efb3..00000000 --- a/packages/react-native-bottom-tabs/src/TabViewAdapter.android.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import NativeTabView from './TabViewNativeComponent'; -import type { TabViewProps } from './TabViewNativeComponent'; -import { StyleSheet, View } from 'react-native'; - -const TabViewAdapter = ({ children, style: _, ...props }: TabViewProps) => { - return ( - - {children} - - ); -}; - -const styles = StyleSheet.create({ - full: { - width: '100%', - height: '100%', - }, -}); - -export default TabViewAdapter; diff --git a/packages/react-native-bottom-tabs/src/TabViewAdapter.tsx b/packages/react-native-bottom-tabs/src/TabViewAdapter.tsx deleted file mode 100644 index 9b07cd35..00000000 --- a/packages/react-native-bottom-tabs/src/TabViewAdapter.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import NativeTabView, { type TabViewProps } from './TabViewNativeComponent'; - -const TabViewAdapter = (props: TabViewProps) => { - return ; -}; - -export default TabViewAdapter; From c4b9119c6517a8f110d472480f943b43ff7b9901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Thu, 16 Jan 2025 13:01:52 +0100 Subject: [PATCH 8/9] feat: use LinearLayout, make measurements better --- .../main/java/com/rcttabview/RCTTabView.kt | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt index 16c34d36..0dc6eb7a 100644 --- a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt +++ b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt @@ -17,6 +17,7 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.widget.FrameLayout +import android.widget.LinearLayout import android.widget.TextView import androidx.viewpager2.widget.ViewPager2 import coil3.ImageLoader @@ -34,7 +35,7 @@ import com.google.android.material.navigation.NavigationBarView.LABEL_VISIBILITY import com.google.android.material.navigation.NavigationBarView.LABEL_VISIBILITY_UNLABELED import com.google.android.material.transition.platform.MaterialFadeThrough -class ReactBottomNavigationView(context: ReactContext) : FrameLayout(context) { +class ReactBottomNavigationView(context: ReactContext) : LinearLayout(context) { private val reactContext: ReactContext = context private val bottomNavigation = BottomNavigationView(context) private val viewPager = ViewPager2(context) @@ -65,23 +66,21 @@ class ReactBottomNavigationView(context: ReactContext) : FrameLayout(context) { .build() init { + orientation = VERTICAL viewPager.adapter = viewPagerAdapter viewPager.isUserInputEnabled = false - viewPager.id = View.generateViewId() addView( viewPager, LayoutParams( LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT - ) + 0, + ).apply { weight = 1f } ) addView(bottomNavigation, LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT - ).apply { - gravity = Gravity.BOTTOM - }) + )) post { addOnLayoutChangeListener { _, left, top, right, bottom, @@ -90,12 +89,8 @@ class ReactBottomNavigationView(context: ReactContext) : FrameLayout(context) { val newHeight = bottom - top if (newWidth != lastReportedSize?.width || newHeight != lastReportedSize?.height) { - // We subtract bottom navigation height from the screen height - // This should be refactored for adaptive navigation - val availableHeight = viewPager.height - bottomNavigation.height - val dpWidth = Utils.convertPixelsToDp(context, viewPager.width) - val dpHeight = Utils.convertPixelsToDp(context, availableHeight) + val dpHeight = Utils.convertPixelsToDp(context, viewPager.height) onNativeLayoutListener?.invoke(dpWidth, dpHeight) lastReportedSize = Size(newWidth, newHeight) From ea7feaf24a0afb41c58f7b2a024dfbf31152ee59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Thu, 16 Jan 2025 16:59:11 +0100 Subject: [PATCH 9/9] feat: migrate off viewpager to frame layout (WIP) --- .../main/java/com/rcttabview/RCTTabView.kt | 105 +++++++++++++----- .../java/com/rcttabview/RCTTabViewImpl.kt | 16 +-- .../java/com/rcttabview/ViewPagerAdapter.kt | 77 ------------- .../android/src/newarch/RCTTabViewManager.kt | 2 +- .../android/src/oldarch/RCTTabViewManager.kt | 12 +- 5 files changed, 87 insertions(+), 125 deletions(-) delete mode 100644 packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/ViewPagerAdapter.kt diff --git a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt index 0dc6eb7a..61bad386 100644 --- a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt +++ b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt @@ -11,7 +11,6 @@ import android.util.Log import android.util.Size import android.util.TypedValue import android.view.Choreographer -import android.view.Gravity import android.view.HapticFeedbackConstants import android.view.MenuItem import android.view.View @@ -19,6 +18,10 @@ import android.view.ViewGroup import android.widget.FrameLayout import android.widget.LinearLayout import android.widget.TextView +import androidx.core.view.children +import androidx.core.view.forEachIndexed +import androidx.core.view.isGone +import androidx.core.view.isVisible import androidx.viewpager2.widget.ViewPager2 import coil3.ImageLoader import coil3.asDrawable @@ -38,15 +41,15 @@ import com.google.android.material.transition.platform.MaterialFadeThrough class ReactBottomNavigationView(context: ReactContext) : LinearLayout(context) { private val reactContext: ReactContext = context private val bottomNavigation = BottomNavigationView(context) - private val viewPager = ViewPager2(context) - val viewPagerAdapter = ViewPagerAdapter() + val layoutHolder = FrameLayout(context) var onTabSelectedListener: ((key: String) -> Unit)? = null var onTabLongPressedListener: ((key: String) -> Unit)? = null var onNativeLayoutListener: ((width: Double, height: Double) -> Unit)? = null - var disablePageTransitions = false + var disablePageAnimations = false var items: MutableList = mutableListOf() + private var isLayoutEnqueued = false private var selectedItem: String? = null private val iconSources: MutableMap = mutableMapOf() private var activeTintColor: Int? = null @@ -67,15 +70,14 @@ class ReactBottomNavigationView(context: ReactContext) : LinearLayout(context) { init { orientation = VERTICAL - viewPager.adapter = viewPagerAdapter - viewPager.isUserInputEnabled = false addView( - viewPager, LayoutParams( + layoutHolder, LayoutParams( LayoutParams.MATCH_PARENT, 0, ).apply { weight = 1f } ) + layoutHolder.isSaveEnabled = false addView(bottomNavigation, LayoutParams( LayoutParams.MATCH_PARENT, @@ -89,8 +91,8 @@ class ReactBottomNavigationView(context: ReactContext) : LinearLayout(context) { val newHeight = bottom - top if (newWidth != lastReportedSize?.width || newHeight != lastReportedSize?.height) { - val dpWidth = Utils.convertPixelsToDp(context, viewPager.width) - val dpHeight = Utils.convertPixelsToDp(context, viewPager.height) + val dpWidth = Utils.convertPixelsToDp(context, layoutHolder.width) + val dpHeight = Utils.convertPixelsToDp(context, layoutHolder.height) onNativeLayoutListener?.invoke(dpWidth, dpHeight) lastReportedSize = Size(newWidth, newHeight) @@ -99,24 +101,12 @@ class ReactBottomNavigationView(context: ReactContext) : LinearLayout(context) { } } - fun setSelectedItem(value: String) { - selectedItem = value - setSelectedIndex(items.indexOfFirst { it.key == value }) - } - - override fun addView(child: View, index: Int, params: ViewGroup.LayoutParams?) { - if (child === viewPager || child === bottomNavigation) { - super.addView(child, index, params) - } else { - viewPagerAdapter.addChild(child, index) - val itemKey = items[index].key - if (selectedItem == itemKey) { - setSelectedIndex(index) - } - } + private val layoutCallback = Choreographer.FrameCallback { + isLayoutEnqueued = false + refreshLayout() } - private val layoutCallback = Choreographer.FrameCallback { + private fun refreshLayout() { measure( MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY), @@ -128,7 +118,8 @@ class ReactBottomNavigationView(context: ReactContext) : LinearLayout(context) { super.requestLayout() @Suppress("SENSELESS_COMPARISON") // layoutCallback can be null here since this method can be called in init - if (layoutCallback != null) { + if (!isLayoutEnqueued && layoutCallback != null) { + isLayoutEnqueued = true // we use NATIVE_ANIMATED_MODULE choreographer queue because it allows us to catch the current // looper loop instead of enqueueing the update in the next loop causing a one frame delay. ReactChoreographer @@ -140,13 +131,69 @@ class ReactBottomNavigationView(context: ReactContext) : LinearLayout(context) { } } + fun setSelectedItem(value: String) { + selectedItem = value + setSelectedIndex(items.indexOfFirst { it.key == value }) + } + + override fun addView(child: View, index: Int, params: ViewGroup.LayoutParams?) { + if (child === layoutHolder || child === bottomNavigation) { + super.addView(child, index, params) + return + } + + val container = createContainer() + child.isEnabled = false + container.addView(child, params) + layoutHolder.addView(container, index) + + val itemKey = items[index].key + if (selectedItem == itemKey) { + setSelectedIndex(index) + refreshLayout() + } + } + + private fun createContainer(): FrameLayout { + val container = FrameLayout(context).apply { + layoutParams = FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT + ) + visibility = INVISIBLE + isEnabled = false + } + return container + } + private fun setSelectedIndex(itemId: Int) { bottomNavigation.selectedItemId = itemId - if (!disablePageTransitions) { + if (!disablePageAnimations) { val fadeThrough = MaterialFadeThrough() - TransitionManager.beginDelayedTransition(this, fadeThrough) + TransitionManager.beginDelayedTransition(layoutHolder, fadeThrough) } - viewPager.setCurrentItem(itemId, false) + + layoutHolder.forEachIndexed { index, view -> + if (itemId == index) { + toggleViewVisibility(view, true) + } else { + toggleViewVisibility(view, false) + } + } + + layoutHolder.requestLayout() + layoutHolder.invalidate() + } + + private fun toggleViewVisibility(view: View, isVisible: Boolean) { + check(view is ViewGroup) { "Native component tree is corrupted." } + + view.visibility = if (isVisible) VISIBLE else INVISIBLE + view.isEnabled = isVisible + + // Container has only 1 child, wrapped React Native view. + val reactNativeView = view.children.first() + reactNativeView.isEnabled = isVisible } private fun onTabSelected(item: MenuItem) { diff --git a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt index 34091117..e8693eb5 100644 --- a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt +++ b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt @@ -96,29 +96,23 @@ class RCTTabViewImpl { } fun getChildCount(parent: ReactBottomNavigationView): Int { - return parent.viewPagerAdapter.itemCount ?: 0 + return parent.layoutHolder.childCount ?: 0 } fun getChildAt(parent: ReactBottomNavigationView, index: Int): View? { - return parent.viewPagerAdapter.getChildAt(index) + return parent.layoutHolder.getChildAt(index) } fun removeView(parent: ReactBottomNavigationView, view: View) { - parent.viewPagerAdapter.removeChild(view) + parent.layoutHolder.removeView(view) } fun removeAllViews(parent: ReactBottomNavigationView) { - parent.viewPagerAdapter.removeAll() + parent.layoutHolder.removeAllViews() } fun removeViewAt(parent: ReactBottomNavigationView, index: Int) { - val child = parent.viewPagerAdapter.getChildAt(index) - - if (child.parent != null) { - (child.parent as? ViewGroup)?.removeView(child) - } - - parent.viewPagerAdapter.removeChildAt(index) + parent.layoutHolder.removeViewAt(index) } fun needsCustomLayoutForChildren(): Boolean { diff --git a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/ViewPagerAdapter.kt b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/ViewPagerAdapter.kt deleted file mode 100644 index 0541dfb3..00000000 --- a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/ViewPagerAdapter.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.rcttabview - -import android.view.View -import android.view.ViewGroup -import android.widget.FrameLayout -import androidx.recyclerview.widget.RecyclerView - -class ViewPagerAdapter : RecyclerView.Adapter() { - private val childrenViews: ArrayList = ArrayList() - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - return ViewHolder(FrameLayout(parent.context).apply { - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - ) - }) - } - - override fun onBindViewHolder(holder: ViewHolder, index: Int) { - val container: FrameLayout = holder.container as FrameLayout - val child = getChildAt(index) - holder.setIsRecyclable(false) - - if (container.childCount > 0) { - container.removeAllViews() - } - - if (child.parent != null) { - (child.parent as FrameLayout).removeView(child) - } - - container.addView(child) - } - - override fun getItemCount(): Int { - return childrenViews.size - } - - fun addChild(child: View, index: Int) { - childrenViews.add(index, child) - notifyItemInserted(index) - } - - fun getChildAt(index: Int): View { - return childrenViews[index] - } - - fun removeChild(child: View) { - val index = childrenViews.indexOf(child) - - if(index > -1) { - removeChildAt(index) - } - } - - fun removeAll() { - for (index in 1..childrenViews.size) { - val child = childrenViews[index-1] - if (child.parent?.parent != null) { - (child.parent.parent as ViewGroup).removeView(child.parent as View) - } - } - val removedChildrenCount = childrenViews.size - childrenViews.clear() - notifyItemRangeRemoved(0, removedChildrenCount) - } - - fun removeChildAt(index: Int) { - if (index >= 0 && index < childrenViews.size) { - childrenViews.removeAt(index) - notifyItemRemoved(index) - } - } - - class ViewHolder(val container: View) : RecyclerView.ViewHolder(container) -} diff --git a/packages/react-native-bottom-tabs/android/src/newarch/RCTTabViewManager.kt b/packages/react-native-bottom-tabs/android/src/newarch/RCTTabViewManager.kt index c6059707..df3866b5 100644 --- a/packages/react-native-bottom-tabs/android/src/newarch/RCTTabViewManager.kt +++ b/packages/react-native-bottom-tabs/android/src/newarch/RCTTabViewManager.kt @@ -138,7 +138,7 @@ class RCTTabViewManager(context: ReactApplicationContext) : } override fun setDisablePageAnimations(view: ReactBottomNavigationView?, value: Boolean) { - view?.disablePageTransitions = value + view?.disablePageAnimations = value } // iOS Methods diff --git a/packages/react-native-bottom-tabs/android/src/oldarch/RCTTabViewManager.kt b/packages/react-native-bottom-tabs/android/src/oldarch/RCTTabViewManager.kt index 230416f7..5703a0d6 100644 --- a/packages/react-native-bottom-tabs/android/src/oldarch/RCTTabViewManager.kt +++ b/packages/react-native-bottom-tabs/android/src/oldarch/RCTTabViewManager.kt @@ -1,7 +1,6 @@ package com.rcttabview import android.view.View -import android.view.ViewGroup import com.facebook.react.module.annotations.ReactModule import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.annotations.ReactProp @@ -78,7 +77,6 @@ class RCTTabViewManager(context: ReactApplicationContext) : ViewGroupManager