Skip to content

Commit a95f6b2

Browse files
committed
feat: take whole screen, refactor measurements
1 parent 640ed84 commit a95f6b2

File tree

8 files changed

+118
-105
lines changed

8 files changed

+118
-105
lines changed

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

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,20 @@ package com.rcttabview
33
import android.annotation.SuppressLint
44
import android.content.Context
55
import android.content.res.ColorStateList
6+
import android.graphics.Color
67
import android.graphics.Typeface
78
import android.graphics.drawable.ColorDrawable
89
import android.graphics.drawable.Drawable
910
import android.os.Build
1011
import android.util.Log
1112
import android.util.TypedValue
1213
import android.view.Choreographer
14+
import android.view.Gravity
1315
import android.view.HapticFeedbackConstants
1416
import android.view.MenuItem
1517
import android.view.View
1618
import android.view.ViewGroup
19+
import android.widget.FrameLayout
1720
import android.widget.TextView
1821
import androidx.appcompat.content.res.AppCompatResources
1922
import coil3.ImageLoader
@@ -25,14 +28,20 @@ import com.facebook.react.views.text.ReactTypefaceUtils
2528
import com.google.android.material.bottomnavigation.BottomNavigationView
2629
import coil3.request.ImageRequest
2730
import coil3.svg.SvgDecoder
31+
import com.google.android.material.navigation.NavigationBarView.LABEL_VISIBILITY_AUTO
32+
import com.google.android.material.navigation.NavigationBarView.LABEL_VISIBILITY_LABELED
33+
import com.google.android.material.navigation.NavigationBarView.LABEL_VISIBILITY_UNLABELED
2834

2935

30-
class ReactBottomNavigationView(context: Context) : BottomNavigationView(context) {
36+
class ReactBottomNavigationView(context: Context) : FrameLayout(context) {
37+
private val bottomNavigation = BottomNavigationView(context)
38+
private val layoutHolder = FrameLayout(context)
3139
private val iconSources: MutableMap<Int, ImageSource> = mutableMapOf()
3240
private var isLayoutEnqueued = false
3341
var items: MutableList<TabInfo>? = null
3442
var onTabSelectedListener: ((key: String) -> Unit)? = null
3543
var onTabLongPressedListener: ((key: String) -> Unit)? = null
44+
var onNativeLayoutListener: ((width: Double, height: Double) -> Unit)? = null
3645
private var activeTintColor: Int? = null
3746
private var inactiveTintColor: Int? = null
3847
private val checkedStateSet = intArrayOf(android.R.attr.state_checked)
@@ -42,6 +51,50 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
4251
private var fontFamily: String? = null
4352
private var fontWeight: Int? = null
4453

54+
init {
55+
val layoutHolderFrameLayout = LayoutParams(
56+
LayoutParams.MATCH_PARENT,
57+
LayoutParams.MATCH_PARENT
58+
)
59+
addView(layoutHolder, layoutHolderFrameLayout)
60+
61+
val bottomNavParams = LayoutParams(
62+
LayoutParams.MATCH_PARENT,
63+
LayoutParams.WRAP_CONTENT
64+
).apply {
65+
gravity = Gravity.BOTTOM
66+
}
67+
68+
addView(bottomNavigation, bottomNavParams)
69+
70+
post {
71+
this.addOnLayoutChangeListener { _, left, top, right, bottom,
72+
oldLeft, oldTop, oldRight, oldBottom ->
73+
val newWidth = right - left
74+
val newHeight = bottom - top
75+
val oldWidth = oldRight - oldLeft
76+
val oldHeight = oldBottom - oldTop
77+
if (newWidth != oldWidth || newHeight != oldHeight) {
78+
val availableHeight = height - bottomNavigation.height
79+
val displayDensity = context.resources.displayMetrics.density
80+
81+
val dpWidth = (width / displayDensity).toDouble()
82+
val dpHeight = (availableHeight / displayDensity).toDouble()
83+
84+
onNativeLayoutListener?.invoke(dpWidth, dpHeight)
85+
}
86+
}
87+
}
88+
}
89+
90+
override fun addView(child: View, index: Int, params: ViewGroup.LayoutParams?) {
91+
if (child === layoutHolder || child === bottomNavigation) {
92+
super.addView(child, index, params)
93+
} else {
94+
layoutHolder.addView(child, params)
95+
}
96+
}
97+
4598
private val imageLoader = ImageLoader.Builder(context)
4699
.components {
47100
add(SvgDecoder.Factory())
@@ -105,11 +158,11 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
105158
}
106159

107160
if (item.badge.isNotEmpty()) {
108-
val badge = this.getOrCreateBadge(index)
161+
val badge = bottomNavigation.getOrCreateBadge(index)
109162
badge.isVisible = true
110163
badge.text = item.badge
111164
} else {
112-
removeBadge(index)
165+
bottomNavigation.removeBadge(index)
113166
}
114167
post {
115168
val itemView = findViewById<View>(menuItem.itemId)
@@ -136,7 +189,7 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
136189
}
137190

138191
private fun getOrCreateItem(index: Int, title: String): MenuItem {
139-
return menu.findItem(index) ?: menu.add(0, index, 0, title)
192+
return bottomNavigation.menu.findItem(index) ?: bottomNavigation.menu.add(0, index, 0, title)
140193
}
141194

142195
fun setIcons(icons: ReadableArray?) {
@@ -155,7 +208,7 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
155208
this.iconSources[idx] = imageSource
156209

157210
// Update existing item if exists.
158-
menu.findItem(idx)?.let { menuItem ->
211+
bottomNavigation.menu.findItem(idx)?.let { menuItem ->
159212
getDrawable(imageSource) {
160213
menuItem.icon = it
161214
}
@@ -164,7 +217,7 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
164217
}
165218

166219
fun setLabeled(labeled: Boolean?) {
167-
labelVisibilityMode = when (labeled) {
220+
bottomNavigation.labelVisibilityMode = when (labeled) {
168221
false -> {
169222
LABEL_VISIBILITY_UNLABELED
170223
}
@@ -178,7 +231,7 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
178231
}
179232

180233
fun setRippleColor(color: ColorStateList) {
181-
itemRippleColor = color
234+
bottomNavigation.itemRippleColor = color
182235
}
183236

184237
@SuppressLint("CheckResult")
@@ -205,7 +258,7 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
205258
// Apply the same color to both active and inactive states
206259
val colorDrawable = ColorDrawable(backgroundColor)
207260

208-
itemBackground = colorDrawable
261+
bottomNavigation.itemBackground = colorDrawable
209262
backgroundTintList = ColorStateList.valueOf(backgroundColor)
210263
}
211264

@@ -220,7 +273,7 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
220273
}
221274

222275
fun setActiveIndicatorColor(color: ColorStateList) {
223-
itemActiveIndicatorColor = color
276+
bottomNavigation.itemActiveIndicatorColor = color
224277
}
225278

226279
fun setHapticFeedback(enabled: Boolean) {
@@ -248,6 +301,10 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
248301
else -> Typeface.NORMAL
249302
}
250303

304+
fun setSelectedItemId(itemId: Int) {
305+
bottomNavigation.selectedItemId = itemId
306+
}
307+
251308
private fun updateTextAppearance() {
252309
if (fontSize != null || fontFamily != null || fontWeight != null) {
253310
val menuView = getChildAt(0) as? ViewGroup ?: return
@@ -293,8 +350,8 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
293350
val colors = intArrayOf(colorSecondary, colorPrimary)
294351

295352
ColorStateList(states, colors).apply {
296-
this@ReactBottomNavigationView.itemTextColor = this
297-
this@ReactBottomNavigationView.itemIconTintList = this
353+
this@ReactBottomNavigationView.bottomNavigation.itemTextColor = this
354+
this@ReactBottomNavigationView.bottomNavigation.itemIconTintList = this
298355
}
299356
}
300357

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

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

33
import android.content.res.ColorStateList
4-
import android.graphics.Color
5-
import androidx.core.view.ViewCompat
6-
import androidx.core.view.WindowInsetsCompat
7-
import com.facebook.react.bridge.ReactContext
84
import com.facebook.react.bridge.ReadableArray
95
import com.facebook.react.common.MapBuilder
6+
import com.rcttabview.events.OnNativeLayoutEvent
7+
import com.rcttabview.events.PageSelectedEvent
8+
import com.rcttabview.events.TabLongPressEvent
109

1110
data class TabInfo(
1211
val key: String,
@@ -43,7 +42,7 @@ class RCTTabViewImpl {
4342

4443
fun setSelectedPage(view: ReactBottomNavigationView, key: String) {
4544
view.items?.indexOfFirst { it.key == key }?.let {
46-
view.selectedItemId = it
45+
view.setSelectedItemId(it)
4746
}
4847
}
4948

@@ -90,32 +89,13 @@ class RCTTabViewImpl {
9089
PageSelectedEvent.EVENT_NAME,
9190
MapBuilder.of("registrationName", "onPageSelected"),
9291
TabLongPressEvent.EVENT_NAME,
93-
MapBuilder.of("registrationName", "onTabLongPress")
92+
MapBuilder.of("registrationName", "onTabLongPress"),
93+
OnNativeLayoutEvent.EVENT_NAME,
94+
MapBuilder.of("registrationName", "onNativeLayout")
9495
)
9596
}
9697

9798
companion object {
9899
const val NAME = "RNCTabView"
99-
100-
// Detect `react-native-edge-to-edge` (https://github.com/zoontek/react-native-edge-to-edge)
101-
private val EDGE_TO_EDGE = try {
102-
Class.forName("com.zoontek.rnedgetoedge.EdgeToEdgePackage")
103-
true
104-
} catch (exception: ClassNotFoundException) {
105-
false
106-
}
107-
108-
fun getNavigationBarInset(context: ReactContext): Int {
109-
val window = context.currentActivity?.window
110-
111-
val isSystemBarTransparent = EDGE_TO_EDGE || window?.navigationBarColor == Color.TRANSPARENT
112-
113-
if (!isSystemBarTransparent) {
114-
return 0
115-
}
116-
117-
val windowInsets = ViewCompat.getRootWindowInsets(window?.decorView ?: return 0)
118-
return windowInsets?.getInsets(WindowInsetsCompat.Type.navigationBars())?.bottom ?: 0
119-
}
120100
}
121101
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.rcttabview.events
2+
3+
import com.facebook.react.bridge.Arguments
4+
import com.facebook.react.uimanager.events.Event
5+
import com.facebook.react.uimanager.events.RCTEventEmitter
6+
7+
class OnNativeLayoutEvent(viewTag: Int, private val width: Double, private val height: Double) :
8+
Event<TabLongPressEvent>(viewTag) {
9+
10+
companion object {
11+
const val EVENT_NAME = "onNativeLayout"
12+
}
13+
14+
override fun getEventName(): String {
15+
return EVENT_NAME
16+
}
17+
18+
override fun dispatch(rctEventEmitter: RCTEventEmitter) {
19+
val event = Arguments.createMap().apply {
20+
putDouble("width", width)
21+
putDouble("height", height)
22+
}
23+
rctEventEmitter.receiveEvent(viewTag, eventName, event)
24+
}
25+
}

packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/PageSelectedEvent.kt renamed to packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/events/PageSelectedEvent.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.rcttabview
1+
package com.rcttabview.events
22

33
import com.facebook.react.bridge.Arguments
44
import com.facebook.react.bridge.WritableMap

packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/TabLongPressedEvent.kt renamed to packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/events/TabLongPressedEvent.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.rcttabview
1+
package com.rcttabview.events
22

33
import com.facebook.react.bridge.Arguments
44
import com.facebook.react.uimanager.events.Event

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

Lines changed: 10 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,19 @@
11
package com.rcttabview
22

3-
import android.view.View.MeasureSpec
4-
import com.facebook.react.bridge.ReadableArray
53
import com.facebook.react.module.annotations.ReactModule
6-
import com.facebook.react.uimanager.LayoutShadowNode
7-
import com.facebook.react.uimanager.SimpleViewManager
84
import com.facebook.react.uimanager.ThemedReactContext
95
import com.facebook.react.uimanager.annotations.ReactProp
106
import com.facebook.react.uimanager.events.EventDispatcher
11-
import com.facebook.yoga.YogaMeasureFunction
12-
import com.facebook.yoga.YogaMeasureMode
13-
import com.facebook.yoga.YogaMeasureOutput
14-
import com.facebook.yoga.YogaNode
157
import com.facebook.react.bridge.ReactApplicationContext
8+
import com.facebook.react.bridge.ReadableArray
169
import com.facebook.react.uimanager.UIManagerModule
10+
import com.facebook.react.uimanager.ViewGroupManager
11+
import com.rcttabview.events.OnNativeLayoutEvent
12+
import com.rcttabview.events.PageSelectedEvent
13+
import com.rcttabview.events.TabLongPressEvent
1714

1815
@ReactModule(name = RCTTabViewImpl.NAME)
19-
class RCTTabViewManager(context: ReactApplicationContext) : SimpleViewManager<ReactBottomNavigationView>() {
16+
class RCTTabViewManager(context: ReactApplicationContext) : ViewGroupManager<ReactBottomNavigationView>() {
2017
private lateinit var eventDispatcher: EventDispatcher
2118
private var tabViewImpl = RCTTabViewImpl()
2219

@@ -34,11 +31,11 @@ class RCTTabViewManager(context: ReactApplicationContext) : SimpleViewManager<Re
3431
view.onTabLongPressedListener = { key ->
3532
eventDispatcher.dispatchEvent(TabLongPressEvent(viewTag = view.id, key))
3633
}
37-
return view
38-
}
3934

40-
override fun createShadowNodeInstance(): LayoutShadowNode {
41-
return TabViewShadowNode()
35+
view.onNativeLayoutListener = { width, height ->
36+
eventDispatcher.dispatchEvent(OnNativeLayoutEvent(viewTag = view.id, width, height))
37+
}
38+
return view
4239
}
4340

4441
override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any>? {
@@ -124,42 +121,4 @@ class RCTTabViewManager(context: ReactApplicationContext) : SimpleViewManager<Re
124121
fun setFontSize(view: ReactBottomNavigationView?, value: Int) {
125122
view?.setFontSize(value)
126123
}
127-
128-
class TabViewShadowNode() : LayoutShadowNode(),
129-
YogaMeasureFunction {
130-
private var mWidth = 0
131-
private var mHeight = 0
132-
private var mMeasured = false
133-
134-
init {
135-
initMeasureFunction()
136-
}
137-
138-
private fun initMeasureFunction() {
139-
setMeasureFunction(this)
140-
}
141-
142-
override fun measure(
143-
node: YogaNode,
144-
width: Float,
145-
widthMode: YogaMeasureMode,
146-
height: Float,
147-
heightMode: YogaMeasureMode
148-
): Long {
149-
if (mMeasured) {
150-
return YogaMeasureOutput.make(mWidth, mHeight)
151-
}
152-
val tabView = ReactBottomNavigationView(themedContext)
153-
val spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
154-
155-
val navigationBarInset = RCTTabViewImpl.getNavigationBarInset(themedContext)
156-
tabView.measure(spec, spec)
157-
// TabBar should always stretch to the width of the screen.
158-
this.mWidth = width.toInt()
159-
this.mHeight = tabView.measuredHeight + navigationBarInset
160-
this.mMeasured = true
161-
162-
return YogaMeasureOutput.make(mWidth, mHeight)
163-
}
164-
}
165124
}

packages/react-native-bottom-tabs/src/TabView.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@ const TabView = <Route extends BaseRoute>({
285285
setTabBarHeight(height);
286286
}}
287287
onNativeLayout={({ nativeEvent: { width, height } }) => {
288+
console.log('onNativeLayout', width, height);
288289
setMeasuredDimensions({ width, height });
289290
}}
290291
hapticFeedbackEnabled={hapticFeedbackEnabled}
@@ -323,7 +324,7 @@ const TabView = <Route extends BaseRoute>({
323324
}
324325
style={
325326
Platform.OS === 'android'
326-
? [StyleSheet.absoluteFill, { zIndex, opacity }]
327+
? [measuredDimensions, { zIndex, opacity }]
327328
: [{ position: 'absolute' }, measuredDimensions]
328329
}
329330
>

0 commit comments

Comments
 (0)