Skip to content

Instantly share code, notes, and snippets.

@yuriyskulskiy
Created September 28, 2024 23:09
Show Gist options
  • Save yuriyskulskiy/e7788aa6619d24061b53ccbf5770e427 to your computer and use it in GitHub Desktop.
Save yuriyskulskiy/e7788aa6619d24061b53ccbf5770e427 to your computer and use it in GitHub Desktop.
//const val ASPECT_RATIO = 4/3f you can play with it
const val ASPECT_RATIO = 1.55f
@Composable
fun ScrollPositionListItem(
itemUi: ListItemUi,
listState: LazyListState,
index: Int,
modifier: Modifier = Modifier,
viewportHeight: Float
) {
val normalizedScrollProgress by remember(listState, viewportHeight) {
derivedStateOf {
val layoutInfo = listState.layoutInfo
val visibleItemInfo = layoutInfo.visibleItemsInfo.firstOrNull { it.index == index }
val itemInfo = visibleItemInfo?.let {
val offsetY = it.offset - layoutInfo.viewportStartOffset
val height = it.size
offsetY to height
} ?: (0 to 0)
val (offsetY, height) = itemInfo
computeNormalizedScrollProgress(offsetY = offsetY, height = height, viewportHeight = viewportHeight)
}
}
ListItem(
itemUi = itemUi,
modifier = modifier,
progress = normalizedScrollProgress,
)
}
@Composable
fun ListItem(
modifier: Modifier = Modifier,
progress: Float,
itemUi: ListItemUi
) {
Card(
colors = CardDefaults.cardColors(containerColor = Color.White),
modifier = modifier
.fillMaxWidth()
.aspectRatio(ASPECT_RATIO),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
shape = RoundedCornerShape(16.dp)
) {
var maxOffset by remember { mutableFloatStateOf(1f) }
var boxHeight by remember { mutableIntStateOf(1) }
val currentVerticalOffset = maxOffset * (0.5f - progress)
Box(modifier = Modifier
.fillMaxSize()
.onGloballyPositioned { coordinates ->
val newHeight = coordinates.size.height
val newWidth = coordinates.size.width
boxHeight = newHeight
maxOffset = (newWidth - newHeight)
.coerceAtLeast(0)
.toFloat()
}
) {
ImageBackLayer(
itemUi = itemUi,
boxHeight = boxHeight,
verticalOffset = currentVerticalOffset
)
TextFrontLayer(
progress = progress,
textRes = itemUi.textRes
)
}
}
}
@Composable
fun ImageBackLayer(
itemUi: ListItemUi,
boxHeight: Int,
verticalOffset: Float
) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(itemUi.drawableRes)
.size(Size(boxHeight, boxHeight))
.scale(Scale.FILL)
.crossfade(true)
.build(),
contentDescription = "Downscaled WebP Image",
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
.offset {
IntOffset(0, verticalOffset.toInt())
},
)
}
@Composable
fun TextFrontLayer(
progress: Float,
@StringRes textRes: Int
) {
// Calculate spacer weights based on scroll progress, ensuring they are always positive
val topWeight = max(0.01f, 1f - progress)
val bottomWeight = max(0.01f, progress)
Column(
horizontalAlignment = Alignment.Start,
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp)
) {
Spacer(modifier = Modifier.weight(topWeight))
Text(
modifier = Modifier
.clip(RoundedCornerShape(12.dp))
.background(color = Color.Black.copy(alpha = 0.4f))
.padding(4.dp),
color = Color.White,
text = stringResource(id = textRes),
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.weight(bottomWeight))
}
}
/**
* Computes the progress of an item as it moves within the viewport.
*
* @param offsetY The vertical offset of the item from the top of the viewport.
* @param height The height of the item.
* @param viewportHeight The height of the viewport.
* @return A normalized progress value from 0 to 1.
*/
fun computeNormalizedScrollProgress(offsetY: Int, height: Int, viewportHeight: Float): Float {
return if (height > 0 && viewportHeight > 0) {
val fullScrollPath = viewportHeight + height
val itemTop = offsetY + height
// Normalized progress calculation from 0 (item bottom at viewport bottom) to 1 (item top at viewport top)
val visiblePortionTop = (fullScrollPath - itemTop) / fullScrollPath
(1f - visiblePortionTop).coerceIn(0f, 1f)
} else 0f
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment