-
-
Notifications
You must be signed in to change notification settings - Fork 588
fix(Android, Fabric): jumping content with native header #2169
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6b2e6da
2a2e1bc
5c8fd35
581cd7e
fd86a96
fd9b32f
bdfc038
1ff7f95
29a92f3
8b445d8
ed0b6ca
342c27e
7d2c5d8
5ed1933
aadfe60
ecd33f6
14b0724
9df3744
b74618b
1c1f8a0
81805f0
f8a251b
29f6b16
6997c26
f67ba5e
27c6ec9
35f41b7
5d9210a
5221637
73e533f
55418d9
eb36e66
61c42d1
f76a0d9
fcc5415
8ea06ac
8e3362f
608a11c
9884e83
699015e
f465673
51ebbb7
e90f57d
d3da357
c3aab0e
175f49c
02a58c6
2c42d65
8a638c4
dc73577
fc701ef
cc110c5
3746958
e3c2fd8
8fb2709
5938d56
359bc49
54463b4
d31dd00
c5e3c6e
184a527
fbda22e
cb6fe2b
db138f9
a0d15bb
4f34895
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
kkafar marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -66,7 +66,8 @@ class Screen(context: ReactContext?) : FabricEnabledViewGroup(context) { | |
val height = b - t | ||
|
||
val headerHeight = calculateHeaderHeight() | ||
val totalHeight = headerHeight.first + headerHeight.second // action bar height + status bar height | ||
val totalHeight = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are there any changes in this file? Please remove the formatting from the PR. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added a comment there and I wouldn't mind if it has stayed. If you are strongly opinionated here I'm willing to open separate PR with that single line comment 😄 |
||
headerHeight.first + headerHeight.second // action bar height + status bar height | ||
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { | ||
updateScreenSizeFabric(width, height, totalHeight) | ||
} else { | ||
|
@@ -171,7 +172,13 @@ class Screen(context: ReactContext?) : FabricEnabledViewGroup(context) { | |
ScreenWindowTraits.applyDidSetStatusBarAppearance() | ||
} | ||
field = statusBarStyle | ||
fragmentWrapper?.let { ScreenWindowTraits.setStyle(this, it.tryGetActivity(), it.tryGetContext()) } | ||
fragmentWrapper?.let { | ||
ScreenWindowTraits.setStyle( | ||
this, | ||
it.tryGetActivity(), | ||
it.tryGetContext() | ||
) | ||
} | ||
} | ||
|
||
var isStatusBarHidden: Boolean? = null | ||
|
@@ -204,7 +211,13 @@ class Screen(context: ReactContext?) : FabricEnabledViewGroup(context) { | |
ScreenWindowTraits.applyDidSetStatusBarAppearance() | ||
} | ||
field = statusBarColor | ||
fragmentWrapper?.let { ScreenWindowTraits.setColor(this, it.tryGetActivity(), it.tryGetContext()) } | ||
fragmentWrapper?.let { | ||
ScreenWindowTraits.setColor( | ||
this, | ||
it.tryGetActivity(), | ||
it.tryGetContext() | ||
) | ||
} | ||
} | ||
|
||
var navigationBarColor: Int? = null | ||
|
@@ -213,7 +226,12 @@ class Screen(context: ReactContext?) : FabricEnabledViewGroup(context) { | |
ScreenWindowTraits.applyDidSetNavigationBarAppearance() | ||
} | ||
field = navigationBarColor | ||
fragmentWrapper?.let { ScreenWindowTraits.setNavigationBarColor(this, it.tryGetActivity()) } | ||
fragmentWrapper?.let { | ||
ScreenWindowTraits.setNavigationBarColor( | ||
this, | ||
it.tryGetActivity() | ||
) | ||
} | ||
} | ||
|
||
var isNavigationBarTranslucent: Boolean? = null | ||
|
@@ -248,20 +266,23 @@ class Screen(context: ReactContext?) : FabricEnabledViewGroup(context) { | |
|
||
private fun calculateHeaderHeight(): Pair<Double, Double> { | ||
val actionBarTv = TypedValue() | ||
val resolvedActionBarSize = context.theme.resolveAttribute(android.R.attr.actionBarSize, actionBarTv, true) | ||
val resolvedActionBarSize = | ||
context.theme.resolveAttribute(android.R.attr.actionBarSize, actionBarTv, true) | ||
|
||
// Check if it's possible to get an attribute from theme context and assign a value from it. | ||
// Otherwise, the default value will be returned. | ||
val actionBarHeight = TypedValue.complexToDimensionPixelSize(actionBarTv.data, resources.displayMetrics) | ||
.takeIf { resolvedActionBarSize && headerConfig?.isHeaderHidden != true && headerConfig?.isHeaderTranslucent != true } | ||
?.let { PixelUtil.toDIPFromPixel(it.toFloat()).toDouble() } ?: 0.0 | ||
|
||
val statusBarHeight = context.resources.getIdentifier("status_bar_height", "dimen", "android") | ||
// Count only status bar when action bar is visible and status bar is not hidden | ||
.takeIf { it > 0 && isStatusBarHidden != true && actionBarHeight > 0 } | ||
?.let { (context.resources::getDimensionPixelSize)(it) } | ||
?.let { PixelUtil.toDIPFromPixel(it.toFloat()).toDouble() } | ||
?: 0.0 | ||
val actionBarHeight = | ||
TypedValue.complexToDimensionPixelSize(actionBarTv.data, resources.displayMetrics) | ||
.takeIf { resolvedActionBarSize && headerConfig?.isHeaderHidden != true && headerConfig?.isHeaderTranslucent != true } | ||
?.let { PixelUtil.toDIPFromPixel(it.toFloat()).toDouble() } ?: 0.0 | ||
|
||
val statusBarHeight = | ||
context.resources.getIdentifier("status_bar_height", "dimen", "android") | ||
// Count only status bar when action bar is visible and status bar is not hidden | ||
.takeIf { it > 0 && isStatusBarHidden != true && actionBarHeight > 0 } | ||
?.let { (context.resources::getDimensionPixelSize)(it) } | ||
?.let { PixelUtil.toDIPFromPixel(it.toFloat()).toDouble() } | ||
?: 0.0 | ||
|
||
return actionBarHeight to statusBarHeight | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,189 @@ | ||
package com.swmansion.rnscreens.utils | ||
|
||
import android.app.Activity | ||
import android.util.Log | ||
import android.view.View | ||
import androidx.appcompat.widget.Toolbar | ||
import androidx.coordinatorlayout.widget.CoordinatorLayout | ||
import com.facebook.react.bridge.ReactApplicationContext | ||
import com.facebook.react.uimanager.PixelUtil | ||
import com.google.android.material.appbar.AppBarLayout | ||
import com.swmansion.rnscreens.ScreenStackHeaderConfig | ||
import java.lang.ref.WeakReference | ||
|
||
/** | ||
* This class provides methods to create dummy layout (that mimics Screen setup), and to compute | ||
* expected header height. It is meant to be accessed from C++ layer via JNI. | ||
* See https://github.com/software-mansion/react-native-screens/pull/2169 | ||
* for more detailed description of the issue this code solves. | ||
*/ | ||
internal class ScreenDummyLayoutHelper(reactContext: ReactApplicationContext) { | ||
// The state required to compute header dimensions. We want this on instance rather than on class | ||
// for context access & being tied to instance lifetime. | ||
private lateinit var coordinatorLayout: CoordinatorLayout | ||
private lateinit var appBarLayout: AppBarLayout | ||
private lateinit var dummyContentView: View | ||
private lateinit var toolbar: Toolbar | ||
private var defaultFontSize: Float = 0f | ||
private var defaultContentInsetStartWithNavigation: Int = 0 | ||
|
||
// LRU with size 1 | ||
private var cache: CacheEntry = CacheEntry.EMPTY | ||
|
||
// We do not want to be responsible for the context lifecycle. If it's null, we're fine. | ||
// This same context is being passed down to our view components so it is destroyed | ||
// only if our views also are. | ||
private var reactContextRef: WeakReference<ReactApplicationContext> = WeakReference(reactContext) | ||
|
||
init { | ||
|
||
// We load the library so that we are able to communicate with our C++ code (descriptor & shadow nodes). | ||
// Basically we leak this object to C++, as its lifecycle should span throughout whole application | ||
// lifecycle anyway. | ||
try { | ||
System.loadLibrary(LIBRARY_NAME) | ||
} catch (e: UnsatisfiedLinkError) { | ||
Log.w(TAG, "Failed to load $LIBRARY_NAME") | ||
} | ||
|
||
WEAK_INSTANCE = WeakReference(this) | ||
ensureDummyLayoutWithHeader(reactContext) | ||
} | ||
|
||
/** | ||
* Initializes dummy view hierarchy with CoordinatorLayout, AppBarLayout and dummy View. | ||
* We utilize this to compute header height (app bar layout height) from C++ layer when its needed. | ||
*/ | ||
private fun ensureDummyLayoutWithHeader(reactContext: ReactApplicationContext) { | ||
if (::coordinatorLayout.isInitialized) { | ||
return | ||
} | ||
|
||
// We need to use activity here, as react context does not have theme attributes required by | ||
// AppBarLayout attached leading to crash. | ||
val contextWithTheme = | ||
requireNotNull(reactContext.currentActivity) { "[RNScreens] Attempt to use context detached from activity" } | ||
|
||
coordinatorLayout = CoordinatorLayout(contextWithTheme) | ||
|
||
appBarLayout = AppBarLayout(contextWithTheme).apply { | ||
layoutParams = CoordinatorLayout.LayoutParams( | ||
CoordinatorLayout.LayoutParams.MATCH_PARENT, | ||
CoordinatorLayout.LayoutParams.WRAP_CONTENT, | ||
) | ||
} | ||
|
||
toolbar = Toolbar(contextWithTheme).apply { | ||
kkafar marked this conversation as resolved.
Show resolved
Hide resolved
|
||
title = DEFAULT_HEADER_TITLE | ||
layoutParams = AppBarLayout.LayoutParams( | ||
AppBarLayout.LayoutParams.MATCH_PARENT, | ||
AppBarLayout.LayoutParams.WRAP_CONTENT | ||
).apply { scrollFlags = 0 } | ||
} | ||
|
||
// We know the title text view will be there, cause we've just set title. | ||
defaultFontSize = ScreenStackHeaderConfig.findTitleTextViewInToolbar(toolbar)!!.textSize | ||
defaultContentInsetStartWithNavigation = toolbar.contentInsetStartWithNavigation | ||
|
||
appBarLayout.addView(toolbar) | ||
|
||
dummyContentView = View(contextWithTheme).apply { | ||
layoutParams = CoordinatorLayout.LayoutParams( | ||
CoordinatorLayout.LayoutParams.MATCH_PARENT, | ||
CoordinatorLayout.LayoutParams.MATCH_PARENT | ||
) | ||
} | ||
|
||
coordinatorLayout.apply { | ||
addView(appBarLayout) | ||
addView(dummyContentView) | ||
} | ||
} | ||
|
||
/** | ||
* Triggers layout pass on dummy view hierarchy, taking into consideration selected | ||
* ScreenStackHeaderConfig props that might have impact on final header height. | ||
* | ||
* @param fontSize font size value as passed from JS | ||
* @return header height in dp as consumed by Yoga | ||
*/ | ||
private fun computeDummyLayout(fontSize: Int, isTitleEmpty: Boolean): Float { | ||
if (!::coordinatorLayout.isInitialized) { | ||
Log.e(TAG, "[RNScreens] Attempt to access dummy view hierarchy before it is initialized") | ||
return 0.0f | ||
} | ||
|
||
if (cache.hasKey(CacheKey(fontSize, isTitleEmpty))) { | ||
return cache.headerHeight | ||
} | ||
|
||
val topLevelDecorView = requireActivity().window.decorView | ||
|
||
// These dimensions are not accurate, as they do include status bar & navigation bar, however | ||
// it is ok for our purposes. | ||
val decorViewWidth = topLevelDecorView.width | ||
val decorViewHeight = topLevelDecorView.height | ||
|
||
val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(decorViewWidth, View.MeasureSpec.EXACTLY) | ||
val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(decorViewHeight, View.MeasureSpec.EXACTLY) | ||
|
||
if (isTitleEmpty) { | ||
toolbar.title = "" | ||
toolbar.contentInsetStartWithNavigation = 0 | ||
} else { | ||
toolbar.title = DEFAULT_HEADER_TITLE | ||
toolbar.contentInsetStartWithNavigation = defaultContentInsetStartWithNavigation | ||
} | ||
|
||
val textView = ScreenStackHeaderConfig.findTitleTextViewInToolbar(toolbar) | ||
textView?.textSize = if (fontSize != FONT_SIZE_UNSET) fontSize.toFloat() else defaultFontSize | ||
|
||
coordinatorLayout.measure(widthMeasureSpec, heightMeasureSpec) | ||
|
||
// It seems that measure pass would be enough, however I'm not certain whether there are no | ||
// scenarios when layout violates measured dimensions. | ||
coordinatorLayout.layout(0, 0, decorViewWidth, decorViewHeight) | ||
|
||
val headerHeight = PixelUtil.toDIPFromPixel(appBarLayout.height.toFloat()) | ||
cache = CacheEntry(CacheKey(fontSize, isTitleEmpty), headerHeight) | ||
return headerHeight | ||
} | ||
|
||
private fun requireReactContext(): ReactApplicationContext { | ||
return requireNotNull(reactContextRef.get()) { "[RNScreens] Attempt to require missing react context" } | ||
} | ||
|
||
private fun requireActivity(): Activity { | ||
return requireNotNull(requireReactContext().currentActivity) { "[RNScreens] Attempt to use context detached from activity" } | ||
} | ||
|
||
companion object { | ||
const val TAG = "ScreenDummyLayoutHelper" | ||
|
||
const val LIBRARY_NAME = "react_codegen_rnscreens" | ||
|
||
const val FONT_SIZE_UNSET = -1 | ||
|
||
private const val DEFAULT_HEADER_TITLE: String = "FontSize123!#$" | ||
|
||
// We access this field from C++ layer, through `getInstance` method. | ||
// We don't care what instance we get access to as long as it has initialized | ||
// dummy view hierarchy. | ||
private var WEAK_INSTANCE = WeakReference<ScreenDummyLayoutHelper>(null) | ||
|
||
@JvmStatic | ||
fun getInstance(): ScreenDummyLayoutHelper? { | ||
return WEAK_INSTANCE.get() | ||
} | ||
} | ||
} | ||
|
||
private data class CacheKey(val fontSize: Int, val isTitleEmpty: Boolean) | ||
|
||
private class CacheEntry(val cacheKey: CacheKey, val headerHeight: Float) { | ||
fun hasKey(key: CacheKey) = cacheKey.fontSize != Int.MIN_VALUE && cacheKey == key | ||
|
||
companion object { | ||
val EMPTY = CacheEntry(CacheKey(Int.MIN_VALUE, false), 0f) | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.