@@ -11,14 +11,17 @@ import android.util.Log
1111import android.util.Size
1212import android.util.TypedValue
1313import android.view.Choreographer
14- import android.view.Gravity
1514import android.view.HapticFeedbackConstants
1615import android.view.MenuItem
1716import android.view.View
1817import android.view.ViewGroup
1918import android.widget.FrameLayout
2019import android.widget.LinearLayout
2120import android.widget.TextView
21+ import androidx.core.view.children
22+ import androidx.core.view.forEachIndexed
23+ import androidx.core.view.isGone
24+ import androidx.core.view.isVisible
2225import androidx.viewpager2.widget.ViewPager2
2326import coil3.ImageLoader
2427import coil3.asDrawable
@@ -38,15 +41,15 @@ import com.google.android.material.transition.platform.MaterialFadeThrough
3841class ReactBottomNavigationView (context : ReactContext ) : LinearLayout(context) {
3942 private val reactContext: ReactContext = context
4043 private val bottomNavigation = BottomNavigationView (context)
41- private val viewPager = ViewPager2 (context)
42- val viewPagerAdapter = ViewPagerAdapter ()
44+ val layoutHolder = FrameLayout (context)
4345
4446 var onTabSelectedListener: ((key: String ) -> Unit )? = null
4547 var onTabLongPressedListener: ((key: String ) -> Unit )? = null
4648 var onNativeLayoutListener: ((width: Double , height: Double ) -> Unit )? = null
47- var disablePageTransitions = false
49+ var disablePageAnimations = false
4850 var items: MutableList <TabInfo > = mutableListOf ()
4951
52+ private var isLayoutEnqueued = false
5053 private var selectedItem: String? = null
5154 private val iconSources: MutableMap <Int , ImageSource > = mutableMapOf ()
5255 private var activeTintColor: Int? = null
@@ -67,15 +70,14 @@ class ReactBottomNavigationView(context: ReactContext) : LinearLayout(context) {
6770
6871 init {
6972 orientation = VERTICAL
70- viewPager.adapter = viewPagerAdapter
71- viewPager.isUserInputEnabled = false
7273
7374 addView(
74- viewPager , LayoutParams (
75+ layoutHolder , LayoutParams (
7576 LayoutParams .MATCH_PARENT ,
7677 0 ,
7778 ).apply { weight = 1f }
7879 )
80+ layoutHolder.isSaveEnabled = false
7981
8082 addView(bottomNavigation, LayoutParams (
8183 LayoutParams .MATCH_PARENT ,
@@ -89,8 +91,8 @@ class ReactBottomNavigationView(context: ReactContext) : LinearLayout(context) {
8991 val newHeight = bottom - top
9092
9193 if (newWidth != lastReportedSize?.width || newHeight != lastReportedSize?.height) {
92- val dpWidth = Utils .convertPixelsToDp(context, viewPager .width)
93- val dpHeight = Utils .convertPixelsToDp(context, viewPager .height)
94+ val dpWidth = Utils .convertPixelsToDp(context, layoutHolder .width)
95+ val dpHeight = Utils .convertPixelsToDp(context, layoutHolder .height)
9496
9597 onNativeLayoutListener?.invoke(dpWidth, dpHeight)
9698 lastReportedSize = Size (newWidth, newHeight)
@@ -99,24 +101,12 @@ class ReactBottomNavigationView(context: ReactContext) : LinearLayout(context) {
99101 }
100102 }
101103
102- fun setSelectedItem (value : String ) {
103- selectedItem = value
104- setSelectedIndex(items.indexOfFirst { it.key == value })
105- }
106-
107- override fun addView (child : View , index : Int , params : ViewGroup .LayoutParams ? ) {
108- if (child == = viewPager || child == = bottomNavigation) {
109- super .addView(child, index, params)
110- } else {
111- viewPagerAdapter.addChild(child, index)
112- val itemKey = items[index].key
113- if (selectedItem == itemKey) {
114- setSelectedIndex(index)
115- }
116- }
104+ private val layoutCallback = Choreographer .FrameCallback {
105+ isLayoutEnqueued = false
106+ refreshLayout()
117107 }
118108
119- private val layoutCallback = Choreographer . FrameCallback {
109+ private fun refreshLayout () {
120110 measure(
121111 MeasureSpec .makeMeasureSpec(width, MeasureSpec .EXACTLY ),
122112 MeasureSpec .makeMeasureSpec(height, MeasureSpec .EXACTLY ),
@@ -128,7 +118,8 @@ class ReactBottomNavigationView(context: ReactContext) : LinearLayout(context) {
128118 super .requestLayout()
129119 @Suppress(" SENSELESS_COMPARISON" ) // layoutCallback can be null here since this method can be called in init
130120
131- if (layoutCallback != null ) {
121+ if (! isLayoutEnqueued && layoutCallback != null ) {
122+ isLayoutEnqueued = true
132123 // we use NATIVE_ANIMATED_MODULE choreographer queue because it allows us to catch the current
133124 // looper loop instead of enqueueing the update in the next loop causing a one frame delay.
134125 ReactChoreographer
@@ -140,13 +131,69 @@ class ReactBottomNavigationView(context: ReactContext) : LinearLayout(context) {
140131 }
141132 }
142133
134+ fun setSelectedItem (value : String ) {
135+ selectedItem = value
136+ setSelectedIndex(items.indexOfFirst { it.key == value })
137+ }
138+
139+ override fun addView (child : View , index : Int , params : ViewGroup .LayoutParams ? ) {
140+ if (child == = layoutHolder || child == = bottomNavigation) {
141+ super .addView(child, index, params)
142+ return
143+ }
144+
145+ val container = createContainer()
146+ child.isEnabled = false
147+ container.addView(child, params)
148+ layoutHolder.addView(container, index)
149+
150+ val itemKey = items[index].key
151+ if (selectedItem == itemKey) {
152+ setSelectedIndex(index)
153+ refreshLayout()
154+ }
155+ }
156+
157+ private fun createContainer (): FrameLayout {
158+ val container = FrameLayout (context).apply {
159+ layoutParams = FrameLayout .LayoutParams (
160+ FrameLayout .LayoutParams .MATCH_PARENT ,
161+ FrameLayout .LayoutParams .MATCH_PARENT
162+ )
163+ visibility = INVISIBLE
164+ isEnabled = false
165+ }
166+ return container
167+ }
168+
143169 private fun setSelectedIndex (itemId : Int ) {
144170 bottomNavigation.selectedItemId = itemId
145- if (! disablePageTransitions ) {
171+ if (! disablePageAnimations ) {
146172 val fadeThrough = MaterialFadeThrough ()
147- TransitionManager .beginDelayedTransition(this , fadeThrough)
173+ TransitionManager .beginDelayedTransition(layoutHolder , fadeThrough)
148174 }
149- viewPager.setCurrentItem(itemId, false )
175+
176+ layoutHolder.forEachIndexed { index, view ->
177+ if (itemId == index) {
178+ toggleViewVisibility(view, true )
179+ } else {
180+ toggleViewVisibility(view, false )
181+ }
182+ }
183+
184+ layoutHolder.requestLayout()
185+ layoutHolder.invalidate()
186+ }
187+
188+ private fun toggleViewVisibility (view : View , isVisible : Boolean ) {
189+ check(view is ViewGroup ) { " Native component tree is corrupted." }
190+
191+ view.visibility = if (isVisible) VISIBLE else INVISIBLE
192+ view.isEnabled = isVisible
193+
194+ // Container has only 1 child, wrapped React Native view.
195+ val reactNativeView = view.children.first()
196+ reactNativeView.isEnabled = isVisible
150197 }
151198
152199 private fun onTabSelected (item : MenuItem ) {
0 commit comments