diff --git a/android/build.gradle b/android/build.gradle index e15468c..afa0f09 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -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()) { diff --git a/android/src/main/java/com/rcttabview/RCTTabView.kt b/android/src/main/java/com/rcttabview/RCTTabView.kt index 2762850..b7ff8ca 100644 --- a/android/src/main/java/com/rcttabview/RCTTabView.kt +++ b/android/src/main/java/com/rcttabview/RCTTabView.kt @@ -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 @@ -11,12 +14,18 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context var items: MutableList? = 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) { @@ -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) { @@ -35,11 +53,18 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context fun updateItems(items: MutableList) { 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 @@ -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) + } } } diff --git a/android/src/main/java/com/rcttabview/RCTTabViewViewManager.kt b/android/src/main/java/com/rcttabview/RCTTabViewViewManager.kt index 6263153..68e2011 100644 --- a/android/src/main/java/com/rcttabview/RCTTabViewViewManager.kt +++ b/android/src/main/java/com/rcttabview/RCTTabViewViewManager.kt @@ -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, @@ -18,7 +25,7 @@ data class TabInfo( @ReactModule(name = RCTTabViewViewManager.NAME) class RCTTabViewViewManager : - ViewGroupManager() { + SimpleViewManager() { private lateinit var eventDispatcher: EventDispatcher override fun getName(): String { @@ -29,15 +36,15 @@ class RCTTabViewViewManager : 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") ?: "", - 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) @@ -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 { @@ -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" } diff --git a/src/TabView.android.tsx b/src/TabView.android.tsx index 9419060..190815b 100644 --- a/src/TabView.android.tsx +++ b/src/TabView.android.tsx @@ -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;