Skip to content

Commit

Permalink
feat: migrate off viewpager to frame layout (WIP)
Browse files Browse the repository at this point in the history
  • Loading branch information
okwasniewski committed Jan 16, 2025
1 parent 30bf2ed commit 2e4089e
Show file tree
Hide file tree
Showing 5 changed files with 45 additions and 112 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,16 @@ 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
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.TextView
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
Expand All @@ -38,15 +40,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<TabInfo> = mutableListOf()

private var isLayoutEnqueued = false
private var selectedItem: String? = null
private val iconSources: MutableMap<Int, ImageSource> = mutableMapOf()
private var activeTintColor: Int? = null
Expand All @@ -67,11 +69,9 @@ 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 }
Expand All @@ -89,8 +89,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)
Expand All @@ -105,18 +105,27 @@ class ReactBottomNavigationView(context: ReactContext) : LinearLayout(context) {
}

override fun addView(child: View, index: Int, params: ViewGroup.LayoutParams?) {
if (child === viewPager || child === bottomNavigation) {
if (child === layoutHolder || child === bottomNavigation) {
super.addView(child, index, params)
} else {
viewPagerAdapter.addChild(child, index)
layoutHolder.addView(child, index)
child.visibility = INVISIBLE
child.isEnabled = false

val itemKey = items[index].key
if (selectedItem == itemKey) {
setSelectedIndex(index)
refreshLayout()
}
}
}

private val layoutCallback = Choreographer.FrameCallback {
isLayoutEnqueued = false
refreshLayout()
}

private fun refreshLayout() {
measure(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY),
Expand All @@ -128,7 +137,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
Expand All @@ -142,11 +152,19 @@ class ReactBottomNavigationView(context: ReactContext) : LinearLayout(context) {

private fun setSelectedIndex(itemId: Int) {
bottomNavigation.selectedItemId = itemId
if (!disablePageTransitions) {
val fadeThrough = MaterialFadeThrough()
TransitionManager.beginDelayedTransition(this, fadeThrough)
// if (!disablePageAnimations) {
// val fadeThrough = MaterialFadeThrough()
// TransitionManager.beginDelayedTransition(layoutHolder, fadeThrough)
// }
layoutHolder.forEachIndexed { index, view ->
if (itemId == index) {
view.visibility = VISIBLE
view.isEnabled = true
} else {
view.visibility = INVISIBLE
view.isEnabled = false
}
}
viewPager.setCurrentItem(itemId, false)
}

private fun onTabSelected(item: MenuItem) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ class RCTTabViewManager(context: ReactApplicationContext) :
}

override fun setDisablePageAnimations(view: ReactBottomNavigationView?, value: Boolean) {
view?.disablePageTransitions = value
view?.disablePageAnimations = value
}

// iOS Methods
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -78,7 +77,6 @@ class RCTTabViewManager(context: ReactApplicationContext) : ViewGroupManager<Rea
tabViewImpl.setSelectedPage(view, key)
}


@ReactProp(name = "labeled")
fun setLabeled(view: ReactBottomNavigationView, flag: Boolean?) {
tabViewImpl.setLabeled(view, flag)
Expand Down Expand Up @@ -114,8 +112,12 @@ class RCTTabViewManager(context: ReactApplicationContext) : ViewGroupManager<Rea
tabViewImpl.setActiveIndicatorColor(view, color)
}

// iOS Props
@ReactProp(name = "disablePageAnimations")
fun setDisablePageAnimations(view: ReactBottomNavigationView, flag: Boolean) {
view.disablePageAnimations = flag
}

// iOS Props
@ReactProp(name = "sidebarAdaptable")
fun setSidebarAdaptable(view: ReactBottomNavigationView, flag: Boolean) {
}
Expand All @@ -124,10 +126,6 @@ class RCTTabViewManager(context: ReactApplicationContext) : ViewGroupManager<Rea
fun setIgnoresTopSafeArea(view: ReactBottomNavigationView, flag: Boolean) {
}

@ReactProp(name = "disablePageAnimations")
fun setDisablePageAnimations(view: ReactBottomNavigationView, flag: Boolean) {
}

@ReactProp(name = "hapticFeedbackEnabled")
fun setHapticFeedbackEnabled(view: ReactBottomNavigationView, value: Boolean) {
tabViewImpl.setHapticFeedbackEnabled(view, value)
Expand Down

0 comments on commit 2e4089e

Please sign in to comment.