Skip to content

Commit

Permalink
Merge pull request #3 from okwasniewski/fix/android-measurements
Browse files Browse the repository at this point in the history
fix: measure views using ShadowNode on Android
  • Loading branch information
okwasniewski authored Sep 30, 2024
2 parents 22845fc + 2baeea5 commit 207a077
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 24 deletions.
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ dependencies {
//noinspection GradleDynamicVersion
implementation "com.facebook.react:react-native:+"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'com.google.android.material:material:1.12.0'
implementation 'com.google.android.material:material:1.13.0-alpha06'
}

if (isNewArchitectureEnabled()) {
Expand Down
44 changes: 41 additions & 3 deletions android/src/main/java/com/rcttabview/RCTTabView.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.rcttabview

import android.content.Context
import android.view.Choreographer
import android.view.MenuItem
import android.view.View
import androidx.appcompat.content.res.AppCompatResources
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.google.android.material.bottomnavigation.BottomNavigationView
Expand All @@ -11,12 +14,18 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
var items: MutableList<TabInfo>? = null

init {
// TODO: Refactor this outside of TabView (attach listener in ViewManager).
setOnItemSelectedListener { item ->
onTabSelected(item)
true
}
}

override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
refreshViewChildrenLayout(this)
}

private fun onTabSelected(item: MenuItem) {
val selectedItem = items?.first { it.title == item.title }
if (selectedItem == null) {
Expand All @@ -27,6 +36,15 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
putString("key", selectedItem.key)
}
onTabSelectedListener?.invoke(event)

// Refresh TabView children to fix issue with animations.
// https://github.com/facebook/react-native/issues/17968#issuecomment-697136929
Choreographer.getInstance().postFrameCallback(object : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
refreshViewChildrenLayout(this@ReactBottomNavigationView)
Choreographer.getInstance().postFrameCallback(this)
}
})
}

fun setOnTabSelectedListener(listener: (WritableMap) -> Unit) {
Expand All @@ -35,11 +53,18 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context

fun updateItems(items: MutableList<TabInfo>) {
this.items = items
// TODO: This doesn't work with hot reload. It clears all menu items
menu.clear()
items.forEachIndexed {index, item ->
// TODO: Handle custom icons
menu.add(0, index, 0, item.title).setIcon(android.R.drawable.btn_star)

val menuItem = menu.add(0, index, 0, item.title)
val iconResourceId = resources.getIdentifier(
item.icon, "drawable", context.packageName
)
if (iconResourceId != 0) {
menuItem.icon = AppCompatResources.getDrawable(context, iconResourceId)
} else {
menuItem.setIcon(android.R.drawable.btn_star) // fallback icon
}
if (item.badge.isNotEmpty()) {
val badge = this.getOrCreateBadge(index)
badge.isVisible = true
Expand All @@ -48,5 +73,18 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
removeBadge(index)
}
}

refreshViewChildrenLayout(this)
}


// Fixes issues with BottomNavigationView children layouting.
private fun refreshViewChildrenLayout(view: View) {
view.post {
view.measure(
View.MeasureSpec.makeMeasureSpec(view.width, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(view.height, View.MeasureSpec.EXACTLY))
view.layout(view.left, view.top, view.right, view.bottom)
}
}
}
72 changes: 59 additions & 13 deletions android/src/main/java/com/rcttabview/RCTTabViewViewManager.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
package com.rcttabview

import com.facebook.react.module.annotations.ReactModule
import android.view.View.MeasureSpec
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.common.MapBuilder
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.UIManagerModule
import com.facebook.react.uimanager.ViewGroupManager
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


data class TabInfo(
val key: String,
Expand All @@ -18,7 +25,7 @@ data class TabInfo(

@ReactModule(name = RCTTabViewViewManager.NAME)
class RCTTabViewViewManager :
ViewGroupManager<ReactBottomNavigationView>() {
SimpleViewManager<ReactBottomNavigationView>() {
private lateinit var eventDispatcher: EventDispatcher

override fun getName(): String {
Expand All @@ -29,15 +36,15 @@ class RCTTabViewViewManager :
fun setItems(view: ReactBottomNavigationView, items: ReadableArray) {
val itemsArray = mutableListOf<TabInfo>()
for (i in 0 until items.size()) {
items.getMap(i)?.let { item ->
itemsArray.add(
TabInfo(
key = item.getString("key") ?: "",
icon = item.getString("icon") ?: "",
title = item.getString("title") ?: "",
badge = item.getString("badge") ?: ""
items.getMap(i).let { item ->
itemsArray.add(
TabInfo(
key = item.getString("key") ?: "",
icon = item.getString("icon") ?: "",
title = item.getString("title") ?: "",
badge = item.getString("badge") ?: ""
)
)
)
}
}
view.updateItems(itemsArray)
Expand All @@ -52,8 +59,6 @@ class RCTTabViewViewManager :

public override fun createViewInstance(context: ThemedReactContext): ReactBottomNavigationView {
eventDispatcher = context.getNativeModule(UIManagerModule::class.java)!!.eventDispatcher
// TODO: BottomBar height is currently set to a constant, this may require Custom Shadow node to measure the view.
// Sometimes the view behaves weird.
val view = ReactBottomNavigationView(context)
view.setOnTabSelectedListener { data ->
data.getString("key")?.let {
Expand All @@ -63,6 +68,47 @@ class RCTTabViewViewManager :
return view
}


class TabViewShadowNode() : LayoutShadowNode(),
YogaMeasureFunction {
private var mWidth = 0
private var mHeight = 0
private var mMeasured = false

init {
initMeasureFunction()
}

private fun initMeasureFunction() {
setMeasureFunction(this)
}

override fun measure(
node: YogaNode,
width: Float,
widthMode: YogaMeasureMode,
height: Float,
heightMode: YogaMeasureMode
): Long {
if (mMeasured) {
return YogaMeasureOutput.make(mWidth, mHeight)
}

val tabView = ReactBottomNavigationView(themedContext)
val spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
tabView.measure(spec, spec)
this.mWidth = tabView.measuredWidth
this.mHeight = tabView.measuredHeight
this.mMeasured = true

return YogaMeasureOutput.make(mWidth, mHeight)
}
}

override fun createShadowNodeInstance(): LayoutShadowNode {
return TabViewShadowNode()
}

companion object {
const val NAME = "RCTTabView"
}
Expand Down
8 changes: 1 addition & 7 deletions src/TabView.android.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,7 @@ const styles = StyleSheet.create({
width: '100%',
height: '100%',
},
tabBar: {
minHeight: 81,
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
},
tabBar: {},
});

export default TabView;

0 comments on commit 207a077

Please sign in to comment.