Skip to content

Commit ea7feaf

Browse files
committed
feat: migrate off viewpager to frame layout (WIP)
1 parent c4b9119 commit ea7feaf

File tree

5 files changed

+87
-125
lines changed

5 files changed

+87
-125
lines changed

packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt

Lines changed: 76 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,17 @@ import android.util.Log
1111
import android.util.Size
1212
import android.util.TypedValue
1313
import android.view.Choreographer
14-
import android.view.Gravity
1514
import android.view.HapticFeedbackConstants
1615
import android.view.MenuItem
1716
import android.view.View
1817
import android.view.ViewGroup
1918
import android.widget.FrameLayout
2019
import android.widget.LinearLayout
2120
import 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
2225
import androidx.viewpager2.widget.ViewPager2
2326
import coil3.ImageLoader
2427
import coil3.asDrawable
@@ -38,15 +41,15 @@ import com.google.android.material.transition.platform.MaterialFadeThrough
3841
class 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) {

packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -96,29 +96,23 @@ class RCTTabViewImpl {
9696
}
9797

9898
fun getChildCount(parent: ReactBottomNavigationView): Int {
99-
return parent.viewPagerAdapter.itemCount ?: 0
99+
return parent.layoutHolder.childCount ?: 0
100100
}
101101

102102
fun getChildAt(parent: ReactBottomNavigationView, index: Int): View? {
103-
return parent.viewPagerAdapter.getChildAt(index)
103+
return parent.layoutHolder.getChildAt(index)
104104
}
105105

106106
fun removeView(parent: ReactBottomNavigationView, view: View) {
107-
parent.viewPagerAdapter.removeChild(view)
107+
parent.layoutHolder.removeView(view)
108108
}
109109

110110
fun removeAllViews(parent: ReactBottomNavigationView) {
111-
parent.viewPagerAdapter.removeAll()
111+
parent.layoutHolder.removeAllViews()
112112
}
113113

114114
fun removeViewAt(parent: ReactBottomNavigationView, index: Int) {
115-
val child = parent.viewPagerAdapter.getChildAt(index)
116-
117-
if (child.parent != null) {
118-
(child.parent as? ViewGroup)?.removeView(child)
119-
}
120-
121-
parent.viewPagerAdapter.removeChildAt(index)
115+
parent.layoutHolder.removeViewAt(index)
122116
}
123117

124118
fun needsCustomLayoutForChildren(): Boolean {

packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/ViewPagerAdapter.kt

Lines changed: 0 additions & 77 deletions
This file was deleted.

packages/react-native-bottom-tabs/android/src/newarch/RCTTabViewManager.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ class RCTTabViewManager(context: ReactApplicationContext) :
138138
}
139139

140140
override fun setDisablePageAnimations(view: ReactBottomNavigationView?, value: Boolean) {
141-
view?.disablePageTransitions = value
141+
view?.disablePageAnimations = value
142142
}
143143

144144
// iOS Methods

packages/react-native-bottom-tabs/android/src/oldarch/RCTTabViewManager.kt

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.rcttabview
22

33
import android.view.View
4-
import android.view.ViewGroup
54
import com.facebook.react.module.annotations.ReactModule
65
import com.facebook.react.uimanager.ThemedReactContext
76
import com.facebook.react.uimanager.annotations.ReactProp
@@ -78,7 +77,6 @@ class RCTTabViewManager(context: ReactApplicationContext) : ViewGroupManager<Rea
7877
tabViewImpl.setSelectedPage(view, key)
7978
}
8079

81-
8280
@ReactProp(name = "labeled")
8381
fun setLabeled(view: ReactBottomNavigationView, flag: Boolean?) {
8482
tabViewImpl.setLabeled(view, flag)
@@ -114,8 +112,12 @@ class RCTTabViewManager(context: ReactApplicationContext) : ViewGroupManager<Rea
114112
tabViewImpl.setActiveIndicatorColor(view, color)
115113
}
116114

117-
// iOS Props
115+
@ReactProp(name = "disablePageAnimations")
116+
fun setDisablePageAnimations(view: ReactBottomNavigationView, flag: Boolean) {
117+
view.disablePageAnimations = flag
118+
}
118119

120+
// iOS Props
119121
@ReactProp(name = "sidebarAdaptable")
120122
fun setSidebarAdaptable(view: ReactBottomNavigationView, flag: Boolean) {
121123
}
@@ -124,10 +126,6 @@ class RCTTabViewManager(context: ReactApplicationContext) : ViewGroupManager<Rea
124126
fun setIgnoresTopSafeArea(view: ReactBottomNavigationView, flag: Boolean) {
125127
}
126128

127-
@ReactProp(name = "disablePageAnimations")
128-
fun setDisablePageAnimations(view: ReactBottomNavigationView, flag: Boolean) {
129-
}
130-
131129
@ReactProp(name = "hapticFeedbackEnabled")
132130
fun setHapticFeedbackEnabled(view: ReactBottomNavigationView, value: Boolean) {
133131
tabViewImpl.setHapticFeedbackEnabled(view, value)

0 commit comments

Comments
 (0)