Skip to content

Commit

Permalink
contact details: add nested scrolling
Browse files Browse the repository at this point in the history
The contact details top section now scrolls up first, followed by any content on the selected tab.

This involves the experimental context receivers feature for better encapsulation.
  • Loading branch information
teobaranga committed Apr 12, 2024
1 parent c05a965 commit 80723f3
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 17 deletions.
3 changes: 3 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ android {
}
kotlinOptions {
jvmTarget = "11"
freeCompilerArgs += arrayOf(
"-Xcontext-receivers",
)
}
buildFeatures {
compose = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import ContactsNavGraph
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
Expand Down Expand Up @@ -34,6 +36,7 @@ import com.teobaranga.monica.contacts.detail.ui.userAvatarItem
import com.teobaranga.monica.ui.PreviewPixel4
import com.teobaranga.monica.ui.avatar.UserAvatar
import com.teobaranga.monica.ui.theme.MonicaTheme
import com.teobaranga.monica.util.compose.nestedScrollParentFirst

@ContactsNavGraph
@Destination
Expand Down Expand Up @@ -74,7 +77,6 @@ private fun ContactDetailScreen(
onBack: () -> Unit,
) {
Scaffold(
modifier = Modifier,
topBar = {
TopAppBar(
title = { },
Expand All @@ -94,19 +96,29 @@ private fun ContactDetailScreen(
val pagerState = rememberPagerState(
pageCount = { contactDetail.infoSections.size },
)
LazyColumn(
//noinspection UnusedBoxWithConstraintsScope - actually used through multiple context receivers
BoxWithConstraints(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
.padding(contentPadding),
horizontalAlignment = Alignment.CenterHorizontally,
.fillMaxSize()
.padding(contentPadding)
) {
userAvatarItem(contactDetail.userAvatar)
fullNameItem(contactDetail.fullName)
infoSectionTabs(
pagerState = pagerState,
infoSections = contactDetail.infoSections,
)
val state = rememberLazyListState()
LazyColumn(
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.fillMaxSize()
.nestedScrollParentFirst(state),
horizontalAlignment = Alignment.CenterHorizontally,
state = state,
) {
userAvatarItem(contactDetail.userAvatar)
fullNameItem(contactDetail.fullName)
infoSectionTabs(
pagerState = pagerState,
infoSections = contactDetail.infoSections,
)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.teobaranga.monica.contacts.detail.ui

import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.BoxWithConstraintsScope
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyListScope
Expand All @@ -25,7 +27,6 @@ fun LazyListScope.userAvatarItem(userAvatar: UserAvatar) {
) {
UserAvatar(
modifier = Modifier
.padding(top = 28.dp)
.size(128.dp),
userAvatar = userAvatar,
onClick = {
Expand All @@ -42,23 +43,22 @@ fun LazyListScope.fullNameItem(fullName: String) {
) {
Text(
modifier = Modifier
.padding(top = 28.dp),
.padding(vertical = 28.dp),
text = fullName,
style = MaterialTheme.typography.headlineMedium,
)
}
}

context(BoxWithConstraintsScope)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
fun LazyListScope.infoSectionTabs(
pagerState: PagerState,
infoSections: List<ContactInfoSection>,
) {
item {
stickyHeader {
val coroutineScope = rememberCoroutineScope()
PrimaryScrollableTabRow(
modifier = Modifier
.padding(top = 28.dp),
selectedTabIndex = pagerState.currentPage,
) {
infoSections.forEachIndexed { index, infoSection ->
Expand All @@ -82,11 +82,15 @@ fun LazyListScope.infoSectionTabs(
item {
HorizontalPager(
modifier = Modifier
.fillParentMaxHeight(),
// Make height just big enough to prevent scrolling behind the TabRow
.height(maxHeight - TabRowHeight),
state = pagerState,
verticalAlignment = Alignment.Top,
) { page ->
infoSections[page].Content(modifier = Modifier)
}
}
}

// In sync with PrimaryNavigationTabTokens.ContainerHeight
private val TabRowHeight = 48.dp
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.teobaranga.monica.util.compose

import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll

/**
* Modifier useful for nested lazy lists where the parent (the list associated with the [state]) should scroll
* before any children.
*/
fun Modifier.nestedScrollParentFirst(state: LazyListState): Modifier {
return this
.composed {
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
if (available.y < 0) {
val consumed = state.dispatchRawDelta(-available.y)
return Offset.Zero.copy(y = -consumed)
} else {
return Offset.Zero
}
}
}
}
nestedScroll(nestedScrollConnection)
}
}

0 comments on commit 80723f3

Please sign in to comment.