Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,25 +1,34 @@
package com.swmansion.rnscreens.gamma.tabs

import android.content.res.Configuration
import android.os.Build
import android.view.Choreographer
import android.view.Gravity
import android.view.MenuItem
import android.view.View
import android.view.WindowInsets
import android.widget.FrameLayout
import android.widget.LinearLayout
import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.view.children
import androidx.fragment.app.FragmentManager
import com.facebook.react.modules.core.ReactChoreographer
import com.facebook.react.uimanager.ThemedReactContext
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.swmansion.rnscreens.BuildConfig
import com.swmansion.rnscreens.gamma.helpers.FragmentManagerHelper
import com.swmansion.rnscreens.gamma.helpers.ViewIdGenerator
import com.swmansion.rnscreens.safearea.EdgeInsets
import com.swmansion.rnscreens.safearea.SafeAreaProvider
import com.swmansion.rnscreens.safearea.SafeAreaView
import com.swmansion.rnscreens.utils.RNSLog
import kotlin.properties.Delegates

class TabsHost(
val reactContext: ThemedReactContext,
) : LinearLayout(reactContext),
TabScreenDelegate {
) : FrameLayout(reactContext),
TabScreenDelegate,
SafeAreaProvider,
View.OnLayoutChangeListener {
/**
* All container updates should go through instance of this class.
* The semantics are as follows:
Expand Down Expand Up @@ -93,19 +102,21 @@ class TabsHost(

private val bottomNavigationView: BottomNavigationView =
BottomNavigationView(wrappedContext).apply {
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
layoutParams =
LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.WRAP_CONTENT,
Gravity.BOTTOM,
)
}

private val contentView: FrameLayout =
FrameLayout(reactContext).apply {
layoutParams =
LinearLayout
.LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.WRAP_CONTENT,
).apply {
weight = 1f
}
LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT,
)
id = ViewIdGenerator.generateViewId()
}

Expand All @@ -121,6 +132,8 @@ class TabsHost(

private var isLayoutEnqueued: Boolean = false

private var interfaceInsetsChangeListener: SafeAreaView? = null

private val appearanceCoordinator =
TabsHostAppearanceCoordinator(wrappedContext, bottomNavigationView, tabScreenFragments)

Expand Down Expand Up @@ -193,7 +206,6 @@ class TabsHost(
}

init {
orientation = VERTICAL
addView(contentView)
addView(bottomNavigationView)

Expand Down Expand Up @@ -418,12 +430,73 @@ class TabsHost(
bottomNavigationView.menu.findItem(index)
}

override fun setOnInterfaceInsetsChangeListener(listener: SafeAreaView) {
if (interfaceInsetsChangeListener == null) {
bottomNavigationView.addOnLayoutChangeListener(this)
}
interfaceInsetsChangeListener = listener
}

override fun removeOnInterfaceInsetsChangeListener(listener: SafeAreaView) {
if (interfaceInsetsChangeListener == listener) {
interfaceInsetsChangeListener = null
bottomNavigationView.removeOnLayoutChangeListener(this)
}
}

override fun getInterfaceInsets(): EdgeInsets = EdgeInsets(0.0f, 0.0f, 0.0f, bottomNavigationView.height.toFloat())

override fun dispatchApplyWindowInsets(insets: WindowInsets?): WindowInsets? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
return super.dispatchApplyWindowInsets(insets)
}

// On Android versions prior to R, insets dispatch is broken.
// In order to mitigate this, we override dispatchApplyWindowInsets with
// correct implementation. To simplify it, we skip the call to TabsHost's
// onApplyWindowInsets.
if (insets?.isConsumed ?: true) {
return insets
}

for (child in children) {
child.dispatchApplyWindowInsets(insets)
}

return insets
}

internal fun onViewManagerAddEventEmitters() {
// When this is called from View Manager the view tag is already set
check(id != NO_ID) { "[RNScreens] TabsHost must have its tag set when registering event emitters" }
eventEmitter = TabsHostEventEmitter(reactContext, id)
}

override fun onLayoutChange(
view: View?,
left: Int,
top: Int,
right: Int,
bottom: Int,
oldLeft: Int,
oldTop: Int,
oldRight: Int,
oldBottom: Int,
) {
require(view is BottomNavigationView) {
"[RNScreens] TabsHost's onLayoutChange expects BottomNavigationView, received $view instead"
}

val oldHeight = oldBottom - oldTop
val newHeight = bottom - top

if (newHeight != oldHeight) {
interfaceInsetsChangeListener?.apply {
this.onInterfaceInsetsChange(EdgeInsets(0.0f, 0.0f, 0.0f, newHeight.toFloat()))
}
}
}

companion object {
const val TAG = "TabsHost"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Implementation adapted from `react-native-safe-area-context`:
// https://github.com/AppAndFlow/react-native-safe-area-context/tree/v5.6.1
package com.swmansion.rnscreens.safearea

import androidx.core.graphics.Insets
import kotlin.math.max

data class EdgeInsets(
val left: Float,
val top: Float,
val right: Float,
val bottom: Float,
) {
companion object {
val ZERO: EdgeInsets = EdgeInsets(0.0f, 0.0f, 0.0f, 0.0f)

fun fromInsets(insets: Insets) =
EdgeInsets(
insets.left.toFloat(),
insets.top.toFloat(),
insets.right.toFloat(),
insets.bottom.toFloat(),
)

fun max(
i1: EdgeInsets,
i2: EdgeInsets,
) = EdgeInsets(
max(i1.left, i2.left),
max(i1.top, i2.top),
max(i1.right, i2.right),
max(i1.bottom, i2.bottom),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.swmansion.rnscreens.safearea

enum class InsetType {
ALL,
SYSTEM,
INTERFACE,
;

fun containsSystem(): Boolean = this == ALL || this == SYSTEM

fun containsInterface(): Boolean = this == ALL || this == INTERFACE
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.swmansion.rnscreens.safearea

/**
* Allows containers that obscure some part of its children views to provide safe area.
*
* This protocol only handles **interface insets** (e.g. toolbar, bottomNavigationView).
* System insets (e.g. `systemBars`, `displayCutout`) are handled through Android inset
* dispatch mechanism (via `onApplyWindowInsets`).
*
* Classes implementing this protocol are responsible for notifying `SafeAreaView`, which
* registers as a listener, about changes to **interface** safe area insets.
*/
interface SafeAreaProvider {
/**
* Responsible for registering **interface** safe area insets listener.
*
* @param listener `SafeAreaView` instance that wants to receive notifications
* about changes to **interface** safe area insets via `onInterfaceInsetsChange`.
*/
fun setOnInterfaceInsetsChangeListener(listener: SafeAreaView)

/**
* Responsible for unregistering **interface** safe area insets listener.
*
* @param listener `SafeAreaView` instance that wants to stop receiving notifications
* about changes to **interface** safe area insets.
*/
fun removeOnInterfaceInsetsChangeListener(listener: SafeAreaView)

/**
* Responsible for providing current **interface** safe area insets.
*
* @returns `EdgeInsets` describing the current **interface** safe area insets.
*/
fun getInterfaceInsets(): EdgeInsets
}
Loading
Loading