I am trying to build a screen with the content starting right behind the TopAppBar, using accompanist-insets. However, the app bar is always transparent.
How to make it a solid/non-transparent navigation bar?
Current Output
Code
val scrollState = rememberLazyListState()
var isScrollStateChanged by remember { mutableStateOf(false) }
isScrollStateChanged = scrollState.firstVisibleItemScrollOffset != 0
val position by animateFloatAsState(if (isScrollStateChanged) 0f else -45f)
val listItems = (1..100).toList()
ProvideWindowInsets {
Surface(
modifier = Modifier
.fillMaxSize()
.systemBarsPadding(),
color = Color.White
) {
Box(Modifier.fillMaxSize()) {
TopAppBar(
content = { Text("Hello nav bar!") },
modifier = Modifier
.offset(0.dp, position.dp)
.alpha(min(1f, 1 + (position / 45f)))
.navigationBarsPadding(bottom = false),
backgroundColor = Color.Red
)
LazyColumn(
state = scrollState
) {
items(items = listItems) {
Text(
"Hello $it",
Modifier
.padding(20.dp)
.fillMaxWidth()
)
}
}
}
}
}
It's doesn't have transparent background. Actually it's LazyColumn has transparent background, and TopAppBar is placed underneath.
Using Box you put items on top of each other, so second one will be under the first one. Reorder them:
Box(Modifier.fillMaxSize()) {
LazyColumn(
state = scrollState
) {
items(items = listItems) {
Text(
"Hello $it",
Modifier
.padding(20.dp)
.fillMaxWidth()
)
}
}
TopAppBar(
content = { Text("Hello nav bar!") },
modifier = Modifier
.offset(0.dp, position.dp)
.alpha(min(1f, 1 + (position / 45f)))
.navigationBarsPadding(bottom = false),
backgroundColor = Color.Red
)
}
Related
What I want
Top fixed item with status bar padding and adaptive radius
Bottom fixed item with navigation bar padding
Adaptive center item, have enough room => Not scrollable, if not => scrollable
Current status
For full demo click this link
Only problem here is when there is no extra space, and we are dragging the scrollable list.
I think it's a bug, because everything is fine except the scrollable column.
window.height - sheetContent.height >= statusBarHeight
Everything is fine.
window.height - sheetContent.height < statusBarHeight
Dragging top fixed item or bottom fixed item, scroll still acting well.
Dragging the scrollable list, sheet pops back to top when the sheetState.offset is approaching statusBarHeight
Test youself
You can test it with these 3 functions, for me, I'm using Pixel 2 Emulator, itemCount at 18,19 could tell the difference.
#Composable
fun CEModalBottomSheet(
sheetState: ModalBottomSheetState,
onCloseDialogClicked: () -> Unit,
title: String = "BottomSheet Title",
toolbarElevation: Dp = 0.dp,
sheetContent: #Composable ColumnScope.() -> Unit,
) {
val density = LocalDensity.current
val sheetOffset = sheetState.offset
val statusBar = WindowInsets.statusBars.asPaddingValues()
val shapeRadius by animateDpAsState(
if (sheetOffset.value < with(density) { statusBar.calculateTopPadding().toPx() }) {
0.dp
} else 12.dp
)
val dynamicStatusBarPadding by remember {
derivedStateOf {
val statusBarHeightPx2 = with(density) { statusBar.calculateTopPadding().toPx() }
val offsetValuePx = sheetOffset.value
if (offsetValuePx >= statusBarHeightPx2) {
0.dp
} else {
with(density) { (statusBarHeightPx2 - offsetValuePx).toDp() }
}
}
}
ModalBottomSheetLayout(
sheetState = sheetState,
sheetShape = RoundedCornerShape(topStart = shapeRadius, topEnd = shapeRadius),
content = {},
sheetContent = {
Column(modifier = Modifier.fillMaxWidth()) {
TopTitleItemForDialog(
title = title,
elevation = toolbarElevation,
topPadding = dynamicStatusBarPadding,
onClick = onCloseDialogClicked
)
sheetContent()
}
})
}
#Composable
fun TopTitleItemForDialog(
title: String,
elevation: Dp = 0.dp,
topPadding: Dp = 0.dp,
onClick: () -> Unit
) {
Surface(
modifier = Modifier.fillMaxWidth(),
color = Color.LightGray,
elevation = elevation
) {
Row(
modifier = Modifier.padding(top = topPadding),
verticalAlignment = Alignment.CenterVertically
) {
Spacer(modifier = Modifier.size(16.dp))
Text(
text = title,
maxLines = 1,
modifier = Modifier.weight(1f)
)
IconButton(onClick = onClick) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(R.string.cancel),
tint = Color.Gray,
modifier = Modifier.size(24.dp)
)
}
}
}
}
class SheetPaddingTestActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.statusBarColor = android.graphics.Color.TRANSPARENT
window.navigationBarColor = android.graphics.Color.TRANSPARENT
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
Box(
Modifier
.fillMaxSize()
.background(Color.Green), contentAlignment = Alignment.Center
) {
var itemCount by remember { mutableStateOf(20) }
val state = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden) { false }
val scope = rememberCoroutineScope()
Text("显示弹窗", modifier = Modifier.clickable {
scope.launch { state.animateTo(ModalBottomSheetValue.Expanded) }
})
CEModalBottomSheet(sheetState = state,
onCloseDialogClicked = {
scope.launch {
state.hide()
}
}, sheetContent = {
Column(
Modifier
.verticalScroll(rememberScrollState())
.weight(1f, fill = false)
.padding(horizontal = 16.dp),
) {
repeat(itemCount) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(30.dp)
.background(Color.Blue.copy(alpha = 1f - it * 0.04f))
)
}
}
CompositionLocalProvider(
LocalContentColor.provides(Color.White)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colors.primary)
.padding(vertical = 12.dp)
.navigationBarsPadding(),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(modifier = Modifier.weight(1f),
onClick = {
itemCount = max(itemCount - 1, 0)
}) {
Icon(Icons.Default.KeyboardArrowLeft, "")
}
Text(
modifier = Modifier.weight(1f), text = "$itemCount",
textAlign = TextAlign.Center
)
IconButton(modifier = Modifier.weight(1f),
onClick = {
itemCount++
}) {
Icon(Icons.Default.KeyboardArrowRight, "")
}
}
}
}
)
}
}
}
}
Whenever I navigate within my screen the LazyColumn scrolls perfectly but the top app bar doesn't move at all. Is it possible to tell the enlarged top app bar to scroll or can this only be done with the 4 default top app bars provided in Material 3?
#Composable
fun MyScreen(navController: NavController) {
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
val mConfiguration = LocalConfiguration.current
val mScreenHeight = mConfiguration.screenHeightDp.dp
val mSize = mScreenHeight / 2
Column {
Box(
modifier = Modifier
.height(mSize)
.weight(1f)
.fillMaxWidth()
) {
LargeTopAppBar(
title = {
Text(
text = "Android Studio Dolphin", overflow = TextOverflow.Visible, maxLines = 1
)
},
scrollBehavior = scrollBehavior)
}
Box(
modifier = Modifier
.height(mSize)
.weight(1f)
.fillMaxWidth()
.background(Color.Green)
) {
MyScreenContent()
// contentPadding ->
// MyScreenContent(contentPadding = contentPadding)
}
}
}
#Composable
fun MyScreenContent(
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues()
) {
Box(modifier = modifier.fillMaxSize()) {
val listState = rememberLazyListState()
LazyColumn(
state = listState,
contentPadding = contentPadding,
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp)
) {
item {
Text(text = "text", style = TextStyle(fontSize = 18.sp))
}
items(75) {
ListItem(it)
}
}
}
}
You should use a Scaffold applying the nestedScroll modifier.
Something like:
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
LargeTopAppBar(
title = {
Text(
text = "Android Studio Dolphin", overflow = TextOverflow.Visible, maxLines = 1
)
},
scrollBehavior = scrollBehavior)
},
content = { innerPadding ->
LazyColumn(contentPadding = innerPadding,){
//....
}
}
)
I want to customize the look of the tabs in jetpack compose, here is my tabs look like right now
But I want to look my tabs like this:
I am creating tabs like this way
TabRow(
selectedTabIndex = pagerState.currentPage,
backgroundColor = MaterialTheme.colors.primary,
contentColor = Color.White
) {
filters.forEachIndexed { index, filter ->
Tab(
text = {
Text(
text = filter.name.replaceFirstChar {
if (it.isLowerCase()) {
it.titlecase(Locale.getDefault())
} else {
it.toString()
}
}
)
},
selected = pagerState.currentPage == index,
onClick = { scope.launch { pagerState.animateScrollToPage(index) } },
)
}
}
How can I achieve that look, I have searched a lot but didn't find any clue, can anyone help?
#Composable
fun CustomTabs() {
var selectedIndex by remember { mutableStateOf(0) }
val list = listOf("Active", "Completed")
TabRow(selectedTabIndex = selectedIndex,
backgroundColor = Color(0xff1E76DA),
modifier = Modifier
.padding(vertical = 4.dp, horizontal = 8.dp)
.clip(RoundedCornerShape(50))
.padding(1.dp),
indicator = { tabPositions: List<TabPosition> ->
Box {}
}
) {
list.forEachIndexed { index, text ->
val selected = selectedIndex == index
Tab(
modifier = if (selected) Modifier
.clip(RoundedCornerShape(50))
.background(
Color.White
)
else Modifier
.clip(RoundedCornerShape(50))
.background(
Color(
0xff1E76DA
)
),
selected = selected,
onClick = { selectedIndex = index },
text = { Text(text = text, color = Color(0xff6FAAEE)) }
)
}
}
}
Result is as in gif.
In addition to Thracian's answer:
If you need to keep the state of tabs after the screen orientation change, use rememberSaveable instead of remember for selectedIndex.
Animated Tabs
Do you want to create something like this?
I tried to use the Material Design 3 library but it makes everything much more difficult, so I created the TabRow from scratch.
You can use this code to save you some time:
#Composable
fun AnimatedTab(
items: List<String>,
modifier: Modifier,
indicatorPadding: Dp = 4.dp,
selectedItemIndex: Int = 0,
onSelectedTab: (index: Int) -> Unit
) {
var tabWidth by remember { mutableStateOf(0.dp) }
val indicatorOffset: Dp by animateDpAsState(
if (selectedItemIndex == 0) {
tabWidth * (selectedItemIndex / items.size.toFloat())
} else {
tabWidth * (selectedItemIndex / items.size.toFloat()) - indicatorPadding
}
)
Box(
modifier = modifier
.onGloballyPositioned { coordinates ->
tabWidth = coordinates.size.width.toDp
}
.background(color = gray100, shape = Shapes.card4)
) {
MyTabIndicator(
modifier = Modifier
.padding(indicatorPadding)
.fillMaxHeight()
.width(tabWidth / items.size - indicatorPadding),
indicatorOffset = indicatorOffset
)
Row(
modifier = Modifier.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly
) {
items.forEachIndexed { index, title ->
MyTabItem(
modifier = Modifier
.fillMaxHeight()
.width(tabWidth / items.size),
onClick = {
onSelectedTab(index)
},
title = title
)
}
}
}
}
#Composable
private fun MyTabIndicator(
modifier: Modifier,
indicatorOffset: Dp,
) {
Box(
modifier = modifier
.offset(x = indicatorOffset)
.clip(Shapes.card4)
.background(white100)
)
}
#Composable
private fun MyTabItem(
modifier: Modifier,
onClick: () -> Unit,
title: String
) {
Box(
modifier = modifier
.clip(Shapes.card4)
.clickable(
interactionSource = MutableInteractionSource(),
indication = null
) { onClick() },
contentAlignment = Alignment.Center
) {
Text(text = title)
}
}
Usage:
var selectedTab by remember { mutableStateOf(0) }
AnimatedTab(
modifier = Modifier
.height(40.dp)
.fillMaxSize(.9f),
selectedItemIndex = selectedTab,
onSelectedTab = { selectedTab = it },
items = listOf("first", "second")
)
I'm writing an android-app using jetpack compose.
This app has a bottom bar which i would like to hide sometimes using an animation.
However, this proved challenging: as soon as i was dealing with a scrollable screen, there was some "jumping" of my ui - see end of post.
My minimal example looks like:
#Preview
#Composable
fun JumpingBottomBarScreen() {
var bottomBarVisible by remember { mutableStateOf(false) }
Scaffold(
content = { padding ->
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.fillMaxWidth()
.background(Color.LightGray)
.padding(padding)
) {
(1..20).forEach { Text(text = "Test #$it of 50") }
Button(
onClick = { bottomBarVisible = !bottomBarVisible },
content = { Text(if (bottomBarVisible) "Hide Bottom Bar" else "Show Bottom Bar") }
)
(21..50).forEach { Text(text = "Test #$it of 50") }
}
},
bottomBar = {
AnimatedVisibility(
visible = bottomBarVisible,
enter = slideInVertically(initialOffsetY = { it }),
exit = slideOutVertically(targetOffsetY = { it })
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.background(Color.Red)
)
}
}
)
}
Avoiding AnimatedVisibility in favor of just offsetting works better, however, i only managed for fixed height bottom-bars, which makes this much less fail-safe.
bottomBar = {
val bottomBarOffset by animateDpAsState(targetValue = if (bottomBarVisible) 0.dp else 50.dp)
Box(
modifier = Modifier
.offset(y = bottomBarOffset)
.fillMaxWidth()
.height(50.dp)
.background(Color.Red)
)
}
How do i do this cleanly?
I'm fine with my screen having more padding at the bottom than expected.
Bad on the left/top, good (but fixed height) on the right/bottom
You need to animate your Column padding as well:
#Composable
fun NotJumpingBottomBarScreen() {
var bottomBarVisible by remember { mutableStateOf(false) }
val bottomBarOffset by animateDpAsState(targetValue = if (bottomBarVisible) 0.dp else 50.dp)
Scaffold(
content = { padding ->
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.fillMaxWidth()
.background(Color.LightGray)
.padding(
start = padding.calculateStartPadding(LocalLayoutDirection.current),
top = padding.calculateTopPadding(),
end = padding.calculateEndPadding(LocalLayoutDirection.current),
bottom = padding.calculateBottomPadding() - bottomBarOffset
)
) {
(1..20).forEach { Text(text = "Test #$it of 50") }
Button(
onClick = { bottomBarVisible = !bottomBarVisible },
content = { Text(if (bottomBarVisible) "Hide Bottom Bar" else "Show Bottom Bar") }
)
(21..50).forEach { Text(text = "Test #$it of 50") }
}
},
bottomBar = {
Box(
modifier = Modifier
.offset(y = bottomBarOffset)
.fillMaxWidth()
.height(50.dp)
.offset(y = bottomBarOffset)
.background(Color.Red)
)
}
)
}
I need to implement LazyColumn with top fading edge effect. On Android I use fade gradient for ListView or RecyclerView, but couldn't find any solution for Jetpack Compose!
I tried to modify canvas:
#Composable
fun Screen() {
Box(
Modifier
.fillMaxWidth()
.background(color = Color.Yellow)
) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.drawWithContent {
val colors = listOf(Color.Transparent, Color.Black)
drawContent()
drawRect(
brush = Brush.verticalGradient(colors),
blendMode = BlendMode.DstIn
)
}
) {
itemsIndexed((1..1000).toList()) { item, index ->
Text(
text = "Item $item: $index value",
modifier = Modifier.padding(12.dp),
color = Color.Red,
fontSize = 24.sp
)
}
}
}
}
But have wrong result:
What you could do is place a Spacer on top of the list, and draw a gradient on that Box. Make the Box small so only a small portion of the list has the overlay. Make the color the same as the background of the screen, and it will look like the content is fading.
val screenBackgroundColor = MaterialTheme.colors.background
Box(Modifier.fillMaxSize()) {
LazyColumn(Modifier.fillMaxSize()) {
//your items
}
//Gradient overlay
Spacer(
Modifier
.fillMaxWidth()
.height(32.dp)
.background(
brush = Brush.verticalGradient(
colors = listOf(
Color.Transparent,
screenBackgroundColor
)
)
)
//.align(Alignment) to control the position of the overlay
)
}
Here's how it would look like:
However, this doesn't seem like quite what you asked for since it seems like you want the actual list content to fade out.
I don't know how you would apply an alpha to only a portion of a view. Perhaps try to dig into the .alpha sources to figure out.
Quick hack which fixes the issue: add .graphicsLayer { alpha = 0.99f } to your modifer
By default Jetpack Compose disables alpha compositing for performance reasons (as explained here; see the "Custom Modifier" section). Without alpha compositing, blend modes which affect transparency (e.g. DstIn) don't have the desired effect. Currently the best workaround is to add .graphicsLayer { alpha = 0.99F } to the modifier on the LazyColumn; this forces Jetpack Compose to enable alpha compositing by making the LazyColumn imperceptibly transparent.
With this change, your code looks like this:
#Composable
fun Screen() {
Box(
Modifier
.fillMaxWidth()
.background(color = Color.Yellow)
) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
// Workaround to enable alpha compositing
.graphicsLayer { alpha = 0.99F }
.drawWithContent {
val colors = listOf(Color.Transparent, Color.Black)
drawContent()
drawRect(
brush = Brush.verticalGradient(colors),
blendMode = BlendMode.DstIn
)
}
) {
itemsIndexed((1..1000).toList()) { item, index ->
Text(
text = "Item $item: $index value",
modifier = Modifier.padding(12.dp),
color = Color.Red,
fontSize = 24.sp
)
}
}
}
}
which produces the correct result
Just a little nudge in the right direction. What this piece of code does is place a Box composable at the top of your LazyColumn with an alpha modifier for fading. You can make multiple of these Box composables in a Column again to create a smoother effect.
#Composable
fun FadingExample() {
Box(
Modifier
.fillMaxWidth()
.requiredHeight(500.dp)) {
LazyColumn(Modifier.fillMaxSize()) {
}
Box(
Modifier
.fillMaxWidth()
.height(10.dp)
.alpha(0.5f)
.background(Color.Transparent)
.align(Alignment.TopCenter)
) {
}
}
}
I optimised the #user3872620 solution. You have just to put this lines below your LazyColumn, VerticalPager.. and just adapt your offset / height, usually offset = height
Box(
Modifier
.fillMaxWidth()
.offset(y= (-10).dp)
.height(10.dp)
.background(brush = Brush.verticalGradient(
colors = listOf(
Color.Transparent,
MaterialTheme.colors.background
)
))
)
You will got this render:
There is the render
This is a very simple implementation of FadingEdgeLazyColumn using AndroidView. Place AndroidView with gradient background applied to the top and bottom of LazyColumn.
#Stable
object GradientDefaults {
#Stable
val Color = androidx.compose.ui.graphics.Color.Black
#Stable
val Height = 30.dp
}
#Stable
sealed class Gradient {
#Immutable
data class Top(
val color: Color = GradientDefaults.Color,
val height: Dp = GradientDefaults.Height,
) : Gradient()
#Immutable
data class Bottom(
val color: Color = GradientDefaults.Color,
val height: Dp = GradientDefaults.Height,
) : Gradient()
}
#Composable
fun FadingEdgeLazyColumn(
modifier: Modifier = Modifier,
gradients: Set<Gradient> = setOf(Gradient.Top(), Gradient.Bottom()),
contentGap: Dp = 0.dp,
state: LazyListState = rememberLazyListState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
reverseLayout: Boolean = false,
verticalArrangement: Arrangement.Vertical =
if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
userScrollEnabled: Boolean = true,
content: LazyListScope.() -> Unit,
) {
val topGradient =
remember(gradients) { gradients.find { it is Gradient.Top } as? Gradient.Top }
val bottomGradient =
remember(gradients) { gradients.find { it is Gradient.Bottom } as? Gradient.Bottom }
ConstraintLayout(modifier = modifier) {
val (topGradientRef, lazyColumnRef, bottomGradientRef) = createRefs()
GradientView(
modifier = Modifier
.constrainAs(topGradientRef) {
top.linkTo(parent.top)
width = Dimension.matchParent
height = Dimension.value(topGradient?.height ?: GradientDefaults.Height)
}
.zIndex(2f),
colors = intArrayOf(
(topGradient?.color ?: GradientDefaults.Color).toArgb(),
Color.Transparent.toArgb()
),
visible = topGradient != null
)
LazyColumn(
modifier = Modifier
.constrainAs(lazyColumnRef) {
top.linkTo(
anchor = topGradientRef.top,
margin = when (topGradient != null) {
true -> contentGap
else -> 0.dp
}
)
bottom.linkTo(
anchor = bottomGradientRef.bottom,
margin = when (bottomGradient != null) {
true -> contentGap
else -> 0.dp
}
)
width = Dimension.matchParent
height = Dimension.fillToConstraints
}
.zIndex(1f),
state = state,
contentPadding = contentPadding,
reverseLayout = reverseLayout,
verticalArrangement = verticalArrangement,
horizontalAlignment = horizontalAlignment,
flingBehavior = flingBehavior,
userScrollEnabled = userScrollEnabled,
content = content
)
GradientView(
modifier = Modifier
.constrainAs(bottomGradientRef) {
bottom.linkTo(parent.bottom)
width = Dimension.matchParent
height = Dimension.value(bottomGradient?.height ?: GradientDefaults.Height)
}
.zIndex(2f),
colors = intArrayOf(
Color.Transparent.toArgb(),
(bottomGradient?.color ?: GradientDefaults.Color).toArgb(),
),
visible = bottomGradient != null
)
}
}
#Composable
private fun GradientView(
modifier: Modifier = Modifier,
#Size(value = 2) colors: IntArray,
visible: Boolean = true,
) {
AndroidView(
modifier = modifier,
factory = { context ->
val gradientBackground = GradientDrawable(
GradientDrawable.Orientation.TOP_BOTTOM,
colors
).apply {
cornerRadius = 0f
}
View(context).apply {
layoutParams = LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT
)
background = gradientBackground
visibility = when (visible) {
true -> View.VISIBLE
else -> View.INVISIBLE
}
}
}
)
}