VerifyScreen: pin scroll shadows to divider edges

Top shadow: 16dp box immediately below the top divider, drawn as a
downward gradient. Appears when scrolled past first item.
Bottom shadow: 16dp box immediately above the bottom divider, drawn as
an upward gradient. Disappears when last item is visible.
Content scrolls under both edges — shadows are structural, not floating.
This commit is contained in:
Kyle Bolen
2026-07-28 05:40:22 +00:00
parent 7aa7e4dae1
commit 0be43f2236

View File

@@ -76,15 +76,16 @@ fun VerifyScreen(state: AppState) {
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
Text("Results", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold) Text("Results", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(4.dp))
HorizontalDivider()
// Scrollable results list with top + bottom shadows // Scrollable results list — shadows are pinned to the divider edges
val listState = rememberLazyListState() val listState = rememberLazyListState()
val topShadowAlpha by remember { val topShadowAlpha by remember {
derivedStateOf { derivedStateOf {
val scrolled = listState.firstVisibleItemScrollOffset + val scrolled = listState.firstVisibleItemScrollOffset +
listState.firstVisibleItemIndex * 80 // approximate listState.firstVisibleItemIndex * 80
(scrolled / 60f).coerceIn(0f, 1f) (scrolled / 60f).coerceIn(0f, 1f)
} }
} }
@@ -93,56 +94,61 @@ fun VerifyScreen(state: AppState) {
val info = listState.layoutInfo val info = listState.layoutInfo
val lastVisible = info.visibleItemsInfo.lastOrNull() val lastVisible = info.visibleItemsInfo.lastOrNull()
val total = info.totalItemsCount val total = info.totalItemsCount
if (lastVisible == null || lastVisible.index >= total - 1) 0f if (lastVisible == null || lastVisible.index >= total - 1) 0f else 1f
else 1f // snap off at bottom is fine here since it's item-based not pixel
} }
} }
// Top edge: shadow drawn below the divider line above the list
if (topShadowAlpha > 0f) {
Box( Box(
modifier = Modifier modifier = Modifier
.weight(1f) .fillMaxWidth()
.height(16.dp)
.drawWithContent { .drawWithContent {
drawContent() drawContent()
// Top shadow — appears when scrolled down
if (topShadowAlpha > 0f) {
drawRect( drawRect(
brush = Brush.verticalGradient( brush = Brush.verticalGradient(
colors = listOf( colors = listOf(
Color.Black.copy(alpha = 0.08f * topShadowAlpha), Color.Black.copy(alpha = 0.10f * topShadowAlpha),
Color.Transparent, Color.Transparent,
), ),
startY = 0f,
endY = 24.dp.toPx(),
) )
) )
} }
// Bottom shadow — fades as last item approaches view
if (bottomShadowAlpha > 0f) {
drawRect(
brush = Brush.verticalGradient(
colors = listOf(
Color.Transparent,
Color.Black.copy(alpha = 0.08f * bottomShadowAlpha),
),
startY = size.height - 24.dp.toPx(),
endY = size.height,
)
) )
} }
}
) {
LazyColumn( LazyColumn(
state = listState, state = listState,
modifier = Modifier.fillMaxSize(), modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(4.dp), verticalArrangement = Arrangement.spacedBy(4.dp),
contentPadding = PaddingValues(vertical = 4.dp),
) { ) {
items(r.results, key = { it.photo.devicePath }) { copyResult -> items(r.results, key = { it.photo.devicePath }) { copyResult ->
CopyResultRow(copyResult) CopyResultRow(copyResult)
} }
} }
// Bottom edge: shadow drawn above the divider line below the list
if (bottomShadowAlpha > 0f) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(16.dp)
.drawWithContent {
drawContent()
drawRect(
brush = Brush.verticalGradient(
colors = listOf(
Color.Transparent,
Color.Black.copy(alpha = 0.10f * bottomShadowAlpha),
),
)
)
}
)
} }
Spacer(Modifier.height(16.dp))
HorizontalDivider() HorizontalDivider()
Spacer(Modifier.height(12.dp)) Spacer(Modifier.height(12.dp))