From ea7feaf24a0afb41c58f7b2a024dfbf31152ee59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Thu, 16 Jan 2025 16:59:11 +0100 Subject: [PATCH] feat: migrate off viewpager to frame layout (WIP) --- .../main/java/com/rcttabview/RCTTabView.kt | 105 +++++++++++++----- .../java/com/rcttabview/RCTTabViewImpl.kt | 16 +-- .../java/com/rcttabview/ViewPagerAdapter.kt | 77 ------------- .../android/src/newarch/RCTTabViewManager.kt | 2 +- .../android/src/oldarch/RCTTabViewManager.kt | 12 +- 5 files changed, 87 insertions(+), 125 deletions(-) delete mode 100644 packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/ViewPagerAdapter.kt diff --git a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt index 0dc6eb7..61bad38 100644 --- a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt +++ b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabView.kt @@ -11,7 +11,6 @@ import android.util.Log import android.util.Size import android.util.TypedValue import android.view.Choreographer -import android.view.Gravity import android.view.HapticFeedbackConstants import android.view.MenuItem import android.view.View @@ -19,6 +18,10 @@ import android.view.ViewGroup import android.widget.FrameLayout import android.widget.LinearLayout import android.widget.TextView +import androidx.core.view.children +import androidx.core.view.forEachIndexed +import androidx.core.view.isGone +import androidx.core.view.isVisible import androidx.viewpager2.widget.ViewPager2 import coil3.ImageLoader import coil3.asDrawable @@ -38,15 +41,15 @@ import com.google.android.material.transition.platform.MaterialFadeThrough class ReactBottomNavigationView(context: ReactContext) : LinearLayout(context) { private val reactContext: ReactContext = context private val bottomNavigation = BottomNavigationView(context) - private val viewPager = ViewPager2(context) - val viewPagerAdapter = ViewPagerAdapter() + val layoutHolder = FrameLayout(context) var onTabSelectedListener: ((key: String) -> Unit)? = null var onTabLongPressedListener: ((key: String) -> Unit)? = null var onNativeLayoutListener: ((width: Double, height: Double) -> Unit)? = null - var disablePageTransitions = false + var disablePageAnimations = false var items: MutableList = mutableListOf() + private var isLayoutEnqueued = false private var selectedItem: String? = null private val iconSources: MutableMap = mutableMapOf() private var activeTintColor: Int? = null @@ -67,15 +70,14 @@ class ReactBottomNavigationView(context: ReactContext) : LinearLayout(context) { init { orientation = VERTICAL - viewPager.adapter = viewPagerAdapter - viewPager.isUserInputEnabled = false addView( - viewPager, LayoutParams( + layoutHolder, LayoutParams( LayoutParams.MATCH_PARENT, 0, ).apply { weight = 1f } ) + layoutHolder.isSaveEnabled = false addView(bottomNavigation, LayoutParams( LayoutParams.MATCH_PARENT, @@ -89,8 +91,8 @@ class ReactBottomNavigationView(context: ReactContext) : LinearLayout(context) { val newHeight = bottom - top if (newWidth != lastReportedSize?.width || newHeight != lastReportedSize?.height) { - val dpWidth = Utils.convertPixelsToDp(context, viewPager.width) - val dpHeight = Utils.convertPixelsToDp(context, viewPager.height) + val dpWidth = Utils.convertPixelsToDp(context, layoutHolder.width) + val dpHeight = Utils.convertPixelsToDp(context, layoutHolder.height) onNativeLayoutListener?.invoke(dpWidth, dpHeight) lastReportedSize = Size(newWidth, newHeight) @@ -99,24 +101,12 @@ class ReactBottomNavigationView(context: ReactContext) : LinearLayout(context) { } } - fun setSelectedItem(value: String) { - selectedItem = value - setSelectedIndex(items.indexOfFirst { it.key == value }) - } - - override fun addView(child: View, index: Int, params: ViewGroup.LayoutParams?) { - if (child === viewPager || child === bottomNavigation) { - super.addView(child, index, params) - } else { - viewPagerAdapter.addChild(child, index) - val itemKey = items[index].key - if (selectedItem == itemKey) { - setSelectedIndex(index) - } - } + private val layoutCallback = Choreographer.FrameCallback { + isLayoutEnqueued = false + refreshLayout() } - private val layoutCallback = Choreographer.FrameCallback { + private fun refreshLayout() { measure( MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY), @@ -128,7 +118,8 @@ class ReactBottomNavigationView(context: ReactContext) : LinearLayout(context) { super.requestLayout() @Suppress("SENSELESS_COMPARISON") // layoutCallback can be null here since this method can be called in init - if (layoutCallback != null) { + if (!isLayoutEnqueued && layoutCallback != null) { + isLayoutEnqueued = true // we use NATIVE_ANIMATED_MODULE choreographer queue because it allows us to catch the current // looper loop instead of enqueueing the update in the next loop causing a one frame delay. ReactChoreographer @@ -140,13 +131,69 @@ class ReactBottomNavigationView(context: ReactContext) : LinearLayout(context) { } } + fun setSelectedItem(value: String) { + selectedItem = value + setSelectedIndex(items.indexOfFirst { it.key == value }) + } + + override fun addView(child: View, index: Int, params: ViewGroup.LayoutParams?) { + if (child === layoutHolder || child === bottomNavigation) { + super.addView(child, index, params) + return + } + + val container = createContainer() + child.isEnabled = false + container.addView(child, params) + layoutHolder.addView(container, index) + + val itemKey = items[index].key + if (selectedItem == itemKey) { + setSelectedIndex(index) + refreshLayout() + } + } + + private fun createContainer(): FrameLayout { + val container = FrameLayout(context).apply { + layoutParams = FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT + ) + visibility = INVISIBLE + isEnabled = false + } + return container + } + private fun setSelectedIndex(itemId: Int) { bottomNavigation.selectedItemId = itemId - if (!disablePageTransitions) { + if (!disablePageAnimations) { val fadeThrough = MaterialFadeThrough() - TransitionManager.beginDelayedTransition(this, fadeThrough) + TransitionManager.beginDelayedTransition(layoutHolder, fadeThrough) } - viewPager.setCurrentItem(itemId, false) + + layoutHolder.forEachIndexed { index, view -> + if (itemId == index) { + toggleViewVisibility(view, true) + } else { + toggleViewVisibility(view, false) + } + } + + layoutHolder.requestLayout() + layoutHolder.invalidate() + } + + private fun toggleViewVisibility(view: View, isVisible: Boolean) { + check(view is ViewGroup) { "Native component tree is corrupted." } + + view.visibility = if (isVisible) VISIBLE else INVISIBLE + view.isEnabled = isVisible + + // Container has only 1 child, wrapped React Native view. + val reactNativeView = view.children.first() + reactNativeView.isEnabled = isVisible } private fun onTabSelected(item: MenuItem) { diff --git a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt index 3409111..e8693eb 100644 --- a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt +++ b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt @@ -96,29 +96,23 @@ class RCTTabViewImpl { } fun getChildCount(parent: ReactBottomNavigationView): Int { - return parent.viewPagerAdapter.itemCount ?: 0 + return parent.layoutHolder.childCount ?: 0 } fun getChildAt(parent: ReactBottomNavigationView, index: Int): View? { - return parent.viewPagerAdapter.getChildAt(index) + return parent.layoutHolder.getChildAt(index) } fun removeView(parent: ReactBottomNavigationView, view: View) { - parent.viewPagerAdapter.removeChild(view) + parent.layoutHolder.removeView(view) } fun removeAllViews(parent: ReactBottomNavigationView) { - parent.viewPagerAdapter.removeAll() + parent.layoutHolder.removeAllViews() } fun removeViewAt(parent: ReactBottomNavigationView, index: Int) { - val child = parent.viewPagerAdapter.getChildAt(index) - - if (child.parent != null) { - (child.parent as? ViewGroup)?.removeView(child) - } - - parent.viewPagerAdapter.removeChildAt(index) + parent.layoutHolder.removeViewAt(index) } fun needsCustomLayoutForChildren(): Boolean { diff --git a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/ViewPagerAdapter.kt b/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/ViewPagerAdapter.kt deleted file mode 100644 index 0541dfb..0000000 --- a/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/ViewPagerAdapter.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.rcttabview - -import android.view.View -import android.view.ViewGroup -import android.widget.FrameLayout -import androidx.recyclerview.widget.RecyclerView - -class ViewPagerAdapter : RecyclerView.Adapter() { - private val childrenViews: ArrayList = ArrayList() - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - return ViewHolder(FrameLayout(parent.context).apply { - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - ) - }) - } - - override fun onBindViewHolder(holder: ViewHolder, index: Int) { - val container: FrameLayout = holder.container as FrameLayout - val child = getChildAt(index) - holder.setIsRecyclable(false) - - if (container.childCount > 0) { - container.removeAllViews() - } - - if (child.parent != null) { - (child.parent as FrameLayout).removeView(child) - } - - container.addView(child) - } - - override fun getItemCount(): Int { - return childrenViews.size - } - - fun addChild(child: View, index: Int) { - childrenViews.add(index, child) - notifyItemInserted(index) - } - - fun getChildAt(index: Int): View { - return childrenViews[index] - } - - fun removeChild(child: View) { - val index = childrenViews.indexOf(child) - - if(index > -1) { - removeChildAt(index) - } - } - - fun removeAll() { - for (index in 1..childrenViews.size) { - val child = childrenViews[index-1] - if (child.parent?.parent != null) { - (child.parent.parent as ViewGroup).removeView(child.parent as View) - } - } - val removedChildrenCount = childrenViews.size - childrenViews.clear() - notifyItemRangeRemoved(0, removedChildrenCount) - } - - fun removeChildAt(index: Int) { - if (index >= 0 && index < childrenViews.size) { - childrenViews.removeAt(index) - notifyItemRemoved(index) - } - } - - class ViewHolder(val container: View) : RecyclerView.ViewHolder(container) -} diff --git a/packages/react-native-bottom-tabs/android/src/newarch/RCTTabViewManager.kt b/packages/react-native-bottom-tabs/android/src/newarch/RCTTabViewManager.kt index c605970..df3866b 100644 --- a/packages/react-native-bottom-tabs/android/src/newarch/RCTTabViewManager.kt +++ b/packages/react-native-bottom-tabs/android/src/newarch/RCTTabViewManager.kt @@ -138,7 +138,7 @@ class RCTTabViewManager(context: ReactApplicationContext) : } override fun setDisablePageAnimations(view: ReactBottomNavigationView?, value: Boolean) { - view?.disablePageTransitions = value + view?.disablePageAnimations = value } // iOS Methods diff --git a/packages/react-native-bottom-tabs/android/src/oldarch/RCTTabViewManager.kt b/packages/react-native-bottom-tabs/android/src/oldarch/RCTTabViewManager.kt index 230416f..5703a0d 100644 --- a/packages/react-native-bottom-tabs/android/src/oldarch/RCTTabViewManager.kt +++ b/packages/react-native-bottom-tabs/android/src/oldarch/RCTTabViewManager.kt @@ -1,7 +1,6 @@ package com.rcttabview import android.view.View -import android.view.ViewGroup import com.facebook.react.module.annotations.ReactModule import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.annotations.ReactProp @@ -78,7 +77,6 @@ class RCTTabViewManager(context: ReactApplicationContext) : ViewGroupManager