Skip to content
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

feat(Android): Add support for different Top App Bar styles #1874

Open
wants to merge 63 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
004f42e
Create first version of large top app bar
tboba Aug 4, 2023
17c4885
Add better prototype of large top app bar
tboba Aug 4, 2023
28e9ccc
Fix missing background in nested scroll view
tboba Aug 4, 2023
0a5412f
Remove unnecessary lines, fix problem with existing view in hierarchy
tboba Aug 7, 2023
b2173fe
Add ability to choose header type in RN
tboba Aug 7, 2023
68c3cf2
Fix issue with not working toolbar after transitioning to another screen
tboba Aug 8, 2023
3188b25
Move headerType prop from ScreenStackHeaderConfig to Screen, fix tran…
tboba Aug 10, 2023
69df636
Make header swipeable
tboba Aug 21, 2023
fa3e5b6
Apply fix for incorrect behavior when navigation bar appearance is set
tboba Aug 22, 2023
2d22607
Apply fix for jumping header
tboba Aug 22, 2023
dddd72f
Remove redundant mToolbar#let
tboba Aug 22, 2023
787c9ac
Remove incorrect inset on large headers
tboba Aug 22, 2023
b45db30
Add `fitsSystemWindows` prop to createNestedScrollViewFromScreen
tboba Aug 22, 2023
bae6092
Apply propagating props from toolbar to collapsing toolbar layout
tboba Aug 23, 2023
390344c
Refactorize way how `collapsingToolbarLayout is being created`
tboba Aug 23, 2023
cf32cbc
Fix not working `headerShown` prop for medium / large headers
tboba Aug 23, 2023
2a35a83
De-addict creating CollapsingToolbarLayout from header mode
tboba Aug 30, 2023
1f7f6a7
Refactor classes related to toolbar management, remove NestedScrollView
tboba Aug 30, 2023
d2f48e8
Revert ScreenStackToolbar class, add support for most of the props to…
tboba Aug 30, 2023
4520dfa
Add fallback values to `getHeightOfToolbar`, change default value of …
tboba Aug 31, 2023
cab619f
Cleanup code, add warning when user changes header type in Screen on …
tboba Sep 1, 2023
4a4c25c
Change misleading comment
tboba Sep 1, 2023
0be4b17
Fix shadow for AppBarLayout on Material 3, add note about bundling Ma…
tboba Sep 1, 2023
d5fa5b0
Remove `getViewTree` utility method
tboba Sep 1, 2023
c4464e5
Add finishing touches for test
tboba Sep 1, 2023
6df79f1
Merge branch 'main' into @tboba/introduce-material3-finally-prettier-…
tboba Sep 1, 2023
dbb8bdc
Add import for Test1874 at FabricTestExample's App.js
tboba Sep 1, 2023
7d144c0
Revert `headerTopInsetEnabled` to false, fix top inset on header
tboba Sep 1, 2023
f63976e
Revert removing private from prop
tboba Sep 1, 2023
7292b92
Cleanup unncessary commits, revert rnsDefaultTargetSdkVersion to 31
tboba Sep 4, 2023
a1d7966
Revert unnecessary changes from Test1844, change default target/compi…
tboba Sep 4, 2023
651b8c3
Comment headerLargeTitle
tboba Sep 4, 2023
4cb1db6
My bad
tboba Sep 4, 2023
c53b773
Add comment to Test1844
tboba Sep 4, 2023
a9942be
Merge branch 'main' into @tboba/introduce-material3-finally-prettier-…
tboba Sep 5, 2023
2bb3e5f
Change `layoutParams` declaration
tboba Sep 5, 2023
76cedb4
Merge branch 'main' into @tboba/introduce-material3-finally-prettier-…
tboba Sep 18, 2023
0a9c345
Merge branch 'main' into @tboba/introduce-material3-finally-prettier-…
tboba Sep 21, 2023
ab0637e
Merge branch 'main' into @tboba/introduce-material3-finally-prettier-…
tboba Sep 26, 2023
c3d2dca
Merge branch 'main' into @tboba/introduce-material3-finally-prettier-…
tboba Jan 3, 2024
493a15c
Fix naming
tboba Jan 3, 2024
4bb960b
Fix issues with using R.attr
tboba Jan 3, 2024
83b0ba3
Remove unncessary from ScreenStackHeaderConfig
tboba Jan 4, 2024
49c0560
Remove center-aligned header type, add headerTItleAlign prop
tboba Jan 4, 2024
925e42c
Fix documentation
tboba Jan 4, 2024
25fc505
Prettify docs, move headerType to the better place
tboba Jan 4, 2024
cdea990
Revert removing adjustment for AppBarLayout, revert removing toolbar
tboba Jan 8, 2024
71c07b9
Move logic of creating toolbars to ScreenStackHeader
tboba Jan 8, 2024
ff4f51d
Create wrapper ScreenStackHeader for combining toolbar and collapsing…
tboba Jan 8, 2024
7168134
Add another screen to the Test1874
tboba Jan 8, 2024
144ddf2
Add ability to change header type during fast-refresh/live, moar exam…
tboba Jan 8, 2024
1b500a3
Remove wrong message about unsupported header type update
tboba Jan 8, 2024
b20c9da
Merge branch 'main' into @tboba/introduce-material3-finally-prettier-…
tboba Jan 29, 2024
4c5612a
Remove fitsSystemWindows from appBarLayout
tboba Feb 12, 2024
a4f4650
Fix docs
tboba Feb 12, 2024
e04221f
Merge branch 'main' into @tboba/introduce-material3-finally-prettier-…
tboba Mar 1, 2024
6659095
Merge branch 'main' into @tboba/introduce-material3-finally-prettier-…
tboba May 8, 2024
2d83af5
Merge branch 'main' into @tboba/introduce-material3-finally-prettier-…
tboba May 8, 2024
bb18c04
Merge branch 'main' into @tboba/introduce-material3-finally-prettier-…
tboba Aug 5, 2024
9d75bd9
Remove leftovers after merge
tboba Aug 5, 2024
88ac350
Update material to 1.9.0, fix config manager delegate
tboba Aug 5, 2024
ad91a46
Fix issue with linting
tboba Aug 5, 2024
848254f
Fix issues with calculating header height
tboba Aug 6, 2024
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
2 changes: 1 addition & 1 deletion Example/android/app/src/main/res/values/styles.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<resources>

<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<style name="AppTheme" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your theme here. -->
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
</style>
Expand Down
2 changes: 1 addition & 1 deletion FabricExample/android/app/src/main/res/values/styles.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<resources>

<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<style name="AppTheme" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your theme here. -->
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
</style>
Expand Down
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ dependencies {
implementation 'androidx.fragment:fragment:1.3.6'
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.google.android.material:material:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation "androidx.core:core-ktx:1.8.0"

constraints {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ abstract class FabricEnabledViewGroup(

private var lastSetWidth = 0f
private var lastSetHeight = 0f
private var lastSetHeaderHeight = 0.0

fun setStateWrapper(wrapper: StateWrapper?) {
mStateWrapper = wrapper
Expand All @@ -42,13 +43,16 @@ abstract class FabricEnabledViewGroup(
// infinite UpdateState/SetState loop.
val delta = 0.9f
if (abs(lastSetWidth - realWidth) < delta &&
abs(lastSetHeight - realHeight) < delta
abs(lastSetHeight - realHeight) < delta &&
abs(lastSetHeaderHeight - headerHeight) < delta
) {
return
}

lastSetWidth = realWidth
lastSetHeight = realHeight
lastSetHeaderHeight = headerHeight

val map: WritableMap =
WritableNativeMap().apply {
putDouble("frameWidth", realWidth.toDouble())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ package com.swmansion.rnscreens

import android.annotation.SuppressLint
import android.content.Context
import androidx.appcompat.widget.Toolbar
import com.google.android.material.appbar.MaterialToolbar
tboba marked this conversation as resolved.
Show resolved Hide resolved

// This class is used to store config closer to search bar
@SuppressLint("ViewConstructor") // Only we construct this view, it is never inflated.
open class CustomToolbar(
context: Context,
val config: ScreenStackHeaderConfig,
) : Toolbar(context)
) : MaterialToolbar(context)
59 changes: 29 additions & 30 deletions android/src/main/java/com/swmansion/rnscreens/Screen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -75,16 +75,15 @@ class Screen(
val width = r - l
val height = b - t

val headerHeight = calculateHeaderHeight()
val totalHeight =
headerHeight.first + headerHeight.second // action bar height + status bar height
// value returned as top will be total height of the header
val topToDp = PixelUtil.toDIPFromPixel(t.toFloat()).toDouble()
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
updateScreenSizeFabric(width, height, totalHeight)
updateScreenSizeFabric(width, height, topToDp)
} else {
updateScreenSizePaper(width, height)
}

notifyHeaderHeightChange(totalHeight)
notifyHeaderHeightChange(topToDp)
}
}

Expand Down Expand Up @@ -238,6 +237,23 @@ class Screen(
}
}

val statusBarInset: Double
get() {
return context.resources
.getIdentifier("status_bar_height", "dimen", "android")
// Count only status bar when it's not hidden
.takeIf { it > 0 && isStatusBarHidden != true }
?.let { (context.resources::getDimensionPixelSize)(it) }
?.let { PixelUtil.toDIPFromPixel(it.toFloat()).toDouble() }
?: 0.0
}

var headerType = HeaderType.Small
set(headerType) {
field = headerType
headerConfig?.onUpdate()
}

var navigationBarColor: Int? = null
set(navigationBarColor) {
if (navigationBarColor != null) {
Expand Down Expand Up @@ -316,31 +332,6 @@ class Screen(
}
}

private fun calculateHeaderHeight(): Pair<Double, Double> {
val actionBarTv = TypedValue()
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

return actionBarHeight to statusBarHeight
}

private fun notifyHeaderHeightChange(headerHeight: Double) {
val screenContext = context as ReactContext
val surfaceId = UIManagerHelper.getSurfaceId(screenContext)
Expand Down Expand Up @@ -388,4 +379,12 @@ class Screen(
NAVIGATION_BAR_TRANSLUCENT,
NAVIGATION_BAR_HIDDEN,
}

enum class HeaderType(
val isCollapsing: Boolean,
) {
Small(false),
Medium(true),
Large(true),
}
}
104 changes: 87 additions & 17 deletions android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package com.swmansion.rnscreens

import android.annotation.SuppressLint
import android.content.Context
import android.content.res.TypedArray
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
Expand All @@ -20,14 +22,17 @@ import com.facebook.react.uimanager.PixelUtil
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.AppBarLayout.ScrollingViewBehavior
import com.swmansion.rnscreens.utils.DeviceUtils
import com.google.android.material.R as MaterialR

class ScreenStackFragment :
ScreenFragment,
ScreenStackFragmentWrapper {
private var appBarLayout: AppBarLayout? = null
private var toolbar: Toolbar? = null
var screenStackHeader = ScreenStackHeader(screen)

private var isToolbarShadowHidden = false
private var isToolbarTranslucent = false
private var isToolbarHidden = false

private var lastFocusedChild: View? = null

Expand All @@ -44,25 +49,43 @@ class ScreenStackFragment :
}

override fun removeToolbar() {
appBarLayout?.let {
toolbar?.let { toolbar ->
if (toolbar.parent === it) {
it.removeView(toolbar)
isToolbarHidden = true

appBarLayout?.let { appBarLayout ->
screenStackHeader.collapsingToolbarLayout?.let { collapsingToolbar ->
if (collapsingToolbar.parent === appBarLayout) {
appBarLayout.removeView(collapsingToolbar)
}
}
}
toolbar = null

screenStackHeader.removeToolbar()

// As AppBarLayout may have dimensions of expanded medium / large header,
// We need to change its layout params to `WRAP_CONTENT`.
appBarLayout?.layoutParams =
CoordinatorLayout.LayoutParams(
CoordinatorLayout.LayoutParams.MATCH_PARENT,
CoordinatorLayout.LayoutParams.WRAP_CONTENT,
)
}

override fun setToolbar(toolbar: Toolbar) {
appBarLayout?.addView(toolbar)
toolbar.layoutParams =
AppBarLayout
.LayoutParams(
AppBarLayout.LayoutParams.MATCH_PARENT,
AppBarLayout.LayoutParams.WRAP_CONTENT,
).apply { scrollFlags = 0 }
this.toolbar = toolbar
isToolbarHidden = false
screenStackHeader.toolbar = toolbar as CustomToolbar
screenStackHeader.recreateToolbar()

screenStackHeader.collapsingToolbarLayout?.apply {
appBarLayout?.addView(this)
}

// As `setToolbar` may be called after changing header's visibility,
// we need to apply correction to layoutParams with proper dimensions.
appBarLayout?.layoutParams =
CoordinatorLayout.LayoutParams(
CoordinatorLayout.LayoutParams.MATCH_PARENT,
getHeightOfToolbar(toolbar.context),
)
}

override fun setToolbarShadowHidden(hidden: Boolean) {
Expand All @@ -78,6 +101,7 @@ class ScreenStackFragment :
val params = screen.layoutParams
(params as CoordinatorLayout.LayoutParams).behavior =
if (translucent) null else ScrollingViewBehavior()

isToolbarTranslucent = translucent
}
}
Expand Down Expand Up @@ -128,19 +152,32 @@ class ScreenStackFragment :
// role. On top of that it breaks screens animations when alfa offscreen compositing is off
// (which is the default)
setBackgroundColor(Color.TRANSPARENT)

layoutParams =
AppBarLayout.LayoutParams(
AppBarLayout.LayoutParams.MATCH_PARENT,
AppBarLayout.LayoutParams.WRAP_CONTENT,
getHeightOfToolbar(context),
)

// On Material 3 the elevation is not visible on AppBarLayout.
// To prevent this behavior, we're setting outline shadow colors to black.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
outlineAmbientShadowColor = Color.BLACK
outlineSpotShadowColor = Color.BLACK
}

screenStackHeader.collapsingToolbarLayout?.let {
addView(recycleView(it))
}
}

view?.addView(appBarLayout)
appBarLayout?.let { view?.addView(it) }

if (isToolbarShadowHidden) {
appBarLayout?.elevation = 0f
appBarLayout?.stateListAnimator = null
}
toolbar?.let { appBarLayout?.addView(recycleView(it)) }

setHasOptionsMenu(true)
return view
}
Expand All @@ -166,6 +203,28 @@ class ScreenStackFragment :
return super.onCreateOptionsMenu(menu, inflater)
}

private fun getHeightOfToolbar(context: Context): Int {
if (isToolbarHidden) {
return CoordinatorLayout.LayoutParams.WRAP_CONTENT
}

val resolvedSize =
when (screen.headerType) {
Screen.HeaderType.Medium -> MaterialR.attr.collapsingToolbarLayoutMediumSize.resolveAttribute(context)
Screen.HeaderType.Large -> MaterialR.attr.collapsingToolbarLayoutLargeSize.resolveAttribute(context)
else -> CoordinatorLayout.LayoutParams.WRAP_CONTENT
}

// For apps that don't support Material 3 it's possible that resolved attribute of
// given header type size will return -1. In such case we want to return fallback value of
// desired header type.
return when (screen.headerType) {
Screen.HeaderType.Medium -> if (resolvedSize != -1) resolvedSize else PixelUtil.toPixelFromDIP(112f).toInt()
Screen.HeaderType.Large -> if (resolvedSize != -1) resolvedSize else PixelUtil.toPixelFromDIP(152f).toInt()
else -> resolvedSize
}
}

private fun shouldShowSearchBar(): Boolean {
val config = screen.headerConfig
val numberOfSubViews = config?.configSubviewsCount ?: 0
Expand Down Expand Up @@ -229,6 +288,17 @@ class ScreenStackFragment :
container.dismiss(this)
}

private fun Int.resolveAttribute(context: Context): Int {
val textSizeAttr = intArrayOf(this)
val indexOfAttrTextSize = 0

val obtainedAttributesTa: TypedArray = context.obtainStyledAttributes(textSizeAttr)
val parsedAttribute = obtainedAttributesTa.getDimensionPixelSize(indexOfAttrTextSize, -1)

obtainedAttributesTa.recycle()
return parsedAttribute
}

Comment on lines +291 to +301
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tried to just resolve attributes with TypedValue (just as in ScreenStackHeaderConfig) but unfortunately I was getting wrong results with it. That's why I've added a helper method for resolving such attributes with TypedArray.

private class ScreensCoordinatorLayout(
context: Context,
private val mFragment: ScreenFragment,
Expand Down
Loading
Loading