Created
July 2, 2025 15:36
-
-
Save Kyriakos-Georgiopoulos/a6a5307a049d4dbbb35c70c3aafc1192 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import androidx.compose.animation.AnimatedContent | |
import androidx.compose.animation.ExperimentalAnimationApi | |
import androidx.compose.animation.animateColorAsState | |
import androidx.compose.animation.core.animateDpAsState | |
import androidx.compose.animation.fadeIn | |
import androidx.compose.animation.fadeOut | |
import androidx.compose.animation.togetherWith | |
import androidx.compose.foundation.Canvas | |
import androidx.compose.foundation.background | |
import androidx.compose.foundation.layout.Arrangement | |
import androidx.compose.foundation.layout.Box | |
import androidx.compose.foundation.layout.Column | |
import androidx.compose.foundation.layout.Row | |
import androidx.compose.foundation.layout.Spacer | |
import androidx.compose.foundation.layout.fillMaxSize | |
import androidx.compose.foundation.layout.fillMaxWidth | |
import androidx.compose.foundation.layout.height | |
import androidx.compose.foundation.layout.padding | |
import androidx.compose.foundation.layout.size | |
import androidx.compose.foundation.pager.HorizontalPager | |
import androidx.compose.foundation.pager.rememberPagerState | |
import androidx.compose.foundation.shape.CircleShape | |
import androidx.compose.material3.Surface | |
import androidx.compose.material3.Text | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.getValue | |
import androidx.compose.ui.Alignment | |
import androidx.compose.ui.Modifier | |
import androidx.compose.ui.draw.clip | |
import androidx.compose.ui.draw.clipToBounds | |
import androidx.compose.ui.geometry.Offset | |
import androidx.compose.ui.graphics.Brush | |
import androidx.compose.ui.graphics.Color | |
import androidx.compose.ui.graphics.ImageBitmap | |
import androidx.compose.ui.graphics.Path | |
import androidx.compose.ui.graphics.drawscope.clipPath | |
import androidx.compose.ui.graphics.drawscope.translate | |
import androidx.compose.ui.graphics.lerp | |
import androidx.compose.ui.platform.LocalConfiguration | |
import androidx.compose.ui.platform.LocalDensity | |
import androidx.compose.ui.res.imageResource | |
import androidx.compose.ui.unit.Dp | |
import androidx.compose.ui.unit.IntOffset | |
import androidx.compose.ui.unit.IntSize | |
import androidx.compose.ui.unit.TextUnit | |
import androidx.compose.ui.unit.dp | |
import androidx.compose.ui.unit.sp | |
import com.zengrip.R | |
private const val DiagonalOffset = 500f | |
@Composable | |
fun DiagonalPager() { | |
val pageCount = 3 | |
val pagerState = rememberPagerState(initialPage = 0) { pageCount } | |
val screenWidth = LocalConfiguration.current.screenWidthDp.dp | |
val screenHeight = LocalConfiguration.current.screenHeightDp.dp | |
val density = LocalDensity.current | |
val widthPx = with(density) { screenWidth.toPx() } | |
val heightPx = with(density) { screenHeight.toPx() } | |
val currentPage = pagerState.currentPage | |
val progress = pagerState.currentPageOffsetFraction.coerceIn(-1f, 1f) | |
Surface(modifier = Modifier.fillMaxSize(), color = Color.Black) { | |
Box(modifier = Modifier.fillMaxSize()) { | |
BackgroundGradient(currentPage, progress) | |
DiagonalPagerImages(pagerState, widthPx, heightPx) | |
PagerOverlay(currentPage, pageCount) | |
} | |
} | |
} | |
@Composable | |
private fun BackgroundGradient(currentPage: Int, progress: Float) { | |
Canvas(modifier = Modifier.fillMaxSize()) { | |
val (startTop, startBottom) = gradientForPage(currentPage) | |
val (endTop, endBottom) = gradientForPage(currentPage + 1) | |
val top = lerp(startTop, endTop, progress) | |
val mid = lerp(startTop, endBottom, progress) | |
val bottom = lerp(startBottom, endBottom, progress) | |
drawRect( | |
brush = Brush.verticalGradient(listOf(top, mid, bottom)), | |
size = size | |
) | |
drawRect( | |
brush = Brush.radialGradient( | |
colors = listOf(Color.White.copy(alpha = 0.05f), Color.Transparent), | |
center = Offset(size.width / 2f, size.height * 0.85f), | |
radius = size.minDimension * 0.8f | |
), | |
size = size | |
) | |
} | |
} | |
@Composable | |
private fun DiagonalPagerImages( | |
pagerState: androidx.compose.foundation.pager.PagerState, | |
width: Float, | |
height: Float | |
) { | |
val currentPage = pagerState.currentPage | |
HorizontalPager( | |
state = pagerState, | |
modifier = Modifier.fillMaxSize(), | |
beyondViewportPageCount = 1, | |
) { page -> | |
val offsetX = when (page) { | |
currentPage -> pagerState.currentPageOffsetFraction * width | |
currentPage - 1 -> (pagerState.currentPageOffsetFraction + 1f) * width | |
currentPage + 1 -> (pagerState.currentPageOffsetFraction - 1f) * width | |
else -> return@HorizontalPager | |
} | |
DiagonalImage( | |
imageRes = imageForPage(page), | |
offset = Offset(offsetX, 0f), | |
width = width, | |
height = height | |
) | |
} | |
} | |
@Composable | |
private fun PagerOverlay(currentPage: Int, pageCount: Int) { | |
Column( | |
modifier = Modifier | |
.fillMaxSize() | |
.padding(horizontal = 24.dp, vertical = 80.dp), | |
verticalArrangement = Arrangement.Bottom, | |
) { | |
Spacer(modifier = Modifier.height(12.dp)) | |
Row( | |
modifier = Modifier.fillMaxWidth(), | |
horizontalArrangement = Arrangement.SpaceBetween, | |
verticalAlignment = Alignment.Bottom | |
) { | |
PagerIndicator( | |
pageCount = pageCount, | |
currentPage = currentPage | |
) | |
Column(horizontalAlignment = Alignment.End) { | |
FadingText("Title for page $currentPage", 24.sp) | |
Spacer(modifier = Modifier.height(4.dp)) | |
FadingText("Some dummy text here.", 16.sp) | |
} | |
} | |
} | |
} | |
@Composable | |
private fun DiagonalImage( | |
imageRes: Int, | |
offset: Offset, | |
width: Float, | |
height: Float | |
) { | |
val imageBitmap = ImageBitmap.imageResource(id = imageRes) | |
Canvas( | |
modifier = Modifier | |
.fillMaxSize() | |
.clipToBounds() | |
) { | |
val midY = height / 1.1f | |
val path = Path().apply { | |
moveTo(0f, 0f) | |
lineTo(width, 0f) | |
lineTo(width, midY - DiagonalOffset) | |
lineTo(0f, midY) | |
close() | |
} | |
val aspectRatio = imageBitmap.width.toFloat() / imageBitmap.height | |
val canvasAspect = size.width / size.height | |
val scaledWidth: Float | |
val scaledHeight: Float | |
if (aspectRatio > canvasAspect) { | |
scaledHeight = size.height | |
scaledWidth = scaledHeight * aspectRatio | |
} else { | |
scaledWidth = size.width | |
scaledHeight = scaledWidth / aspectRatio | |
} | |
val left = (size.width - scaledWidth) / 2f | |
translate(offset.x, offset.y) { | |
clipPath(path) { | |
drawImage( | |
image = imageBitmap, | |
dstSize = IntSize(scaledWidth.toInt(), scaledHeight.toInt()), | |
dstOffset = IntOffset(left.toInt(), 0) | |
) | |
} | |
} | |
} | |
} | |
@Composable | |
private fun PagerIndicator( | |
pageCount: Int, | |
currentPage: Int, | |
activeColor: Color = Color.White, | |
inactiveColor: Color = Color.White.copy(alpha = 0.3f), | |
activeSize: Dp = 12.dp, | |
inactiveSize: Dp = 8.dp, | |
spacing: Dp = 8.dp | |
) { | |
Row( | |
horizontalArrangement = Arrangement.spacedBy(spacing), | |
verticalAlignment = Alignment.CenterVertically | |
) { | |
repeat(pageCount) { index -> | |
val isSelected = index == currentPage | |
val dotSize by animateDpAsState( | |
targetValue = if (isSelected) activeSize else inactiveSize, | |
label = "dotSize" | |
) | |
val dotColor by animateColorAsState( | |
targetValue = if (isSelected) activeColor else inactiveColor, | |
label = "dotColor" | |
) | |
Box( | |
modifier = Modifier | |
.size(dotSize) | |
.clip(CircleShape) | |
.background(dotColor) | |
) | |
} | |
} | |
} | |
@OptIn(ExperimentalAnimationApi::class) | |
@Composable | |
private fun FadingText(text: String, fontSize: TextUnit) { | |
AnimatedContent( | |
targetState = text, | |
transitionSpec = { fadeIn() togetherWith fadeOut() }, | |
label = "fadingText" | |
) { | |
Text(text = it, fontSize = fontSize, color = Color.White) | |
} | |
} | |
@Composable | |
private fun imageForPage(page: Int): Int = when (page % 3) { | |
0 -> R.drawable.lion | |
1 -> R.drawable.elephant | |
else -> R.drawable.tiger | |
} | |
private fun gradientForPage(page: Int): Pair<Color, Color> = when (page % 3) { | |
0 -> Color(0xFF4A4A4A) to Color(0xFF2F2F2F) | |
1 -> Color(0xFF5A5A5A) to Color(0xFF3A3A3A) | |
else -> Color(0xFF3C3C3C) to Color(0xFF252525) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment