Skip to content

Instantly share code, notes, and snippets.

@Kyriakos-Georgiopoulos
Last active October 28, 2025 11:08
Show Gist options
  • Select an option

  • Save Kyriakos-Georgiopoulos/a6a5307a049d4dbbb35c70c3aafc1192 to your computer and use it in GitHub Desktop.

Select an option

Save Kyriakos-Georgiopoulos/a6a5307a049d4dbbb35c70c3aafc1192 to your computer and use it in GitHub Desktop.

Revisions

  1. Kyriakos-Georgiopoulos revised this gist Oct 28, 2025. 1 changed file with 16 additions and 0 deletions.
    16 changes: 16 additions & 0 deletions DiagonalPager.kt
    Original 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
  2. Kyriakos-Georgiopoulos created this gist Jul 2, 2025.
    269 changes: 269 additions & 0 deletions DiagonalPager.kt
    Original 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)
    }