Last active
October 28, 2025 11:08
-
-
Save Kyriakos-Georgiopoulos/a6a5307a049d4dbbb35c70c3aafc1192 to your computer and use it in GitHub Desktop.
Revisions
-
Kyriakos-Georgiopoulos revised this gist
Oct 28, 2025 . 1 changed file with 16 additions and 0 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -1,3 +1,19 @@ /* * Copyright 2025 Kyriakos Georgiopoulos * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import androidx.compose.animation.AnimatedContent import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.animateColorAsState -
Kyriakos-Georgiopoulos created this gist
Jul 2, 2025 .There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,269 @@ 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) }