Skip to content

Commit 601f0b1

Browse files
authored
Merge pull request #944 from tunjid/tj/gallery-indicators
Add indicator for gallery
2 parents 1a34fe0 + 60ef3e1 commit 601f0b1

File tree

2 files changed

+134
-1
lines changed

2 files changed

+134
-1
lines changed

feature/gallery/src/commonMain/kotlin/com/tunjid/heron/gallery/GalleryScreen.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import androidx.compose.foundation.layout.aspectRatio
2525
import androidx.compose.foundation.layout.fillMaxSize
2626
import androidx.compose.foundation.layout.fillMaxWidth
2727
import androidx.compose.foundation.layout.navigationBars
28+
import androidx.compose.foundation.layout.navigationBarsPadding
2829
import androidx.compose.foundation.layout.padding
2930
import androidx.compose.foundation.layout.statusBarsPadding
3031
import androidx.compose.foundation.layout.windowInsetsPadding
@@ -62,6 +63,7 @@ import com.tunjid.heron.gallery.ui.GalleryFooter
6263
import com.tunjid.heron.gallery.ui.GalleryImage
6364
import com.tunjid.heron.gallery.ui.GalleryVideo
6465
import com.tunjid.heron.gallery.ui.ImageDownloadState
66+
import com.tunjid.heron.gallery.ui.Indicator
6567
import com.tunjid.heron.gallery.ui.MediaInteractions
6668
import com.tunjid.heron.gallery.ui.MediaOverlay
6769
import com.tunjid.heron.gallery.ui.MediaPoster
@@ -395,6 +397,14 @@ private fun HorizontalItems(
395397
},
396398
)
397399

400+
Indicator(
401+
modifier = Modifier
402+
.align(Alignment.BottomCenter)
403+
.padding(bottom = 36.dp)
404+
.navigationBarsPadding(),
405+
pagerState = pagerState,
406+
)
407+
398408
MediaOverlay(
399409
modifier = Modifier
400410
.fillMaxSize(),
@@ -545,6 +555,6 @@ private fun ScrollableState.isConstrainedBy(
545555
return constrainedAtStart || constrainedAtEnd
546556
}
547557

548-
private val HorizontalDragToPopSlop = 20.dp
558+
private val HorizontalDragToPopSlop = 16.dp
549559
private const val MediaZIndex = 0f
550560
private const val PagerPrefetchCount = 1
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* Copyright 2024 Adetunji Dahunsi
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.tunjid.heron.gallery.ui
18+
19+
import androidx.compose.foundation.Canvas
20+
import androidx.compose.foundation.layout.height
21+
import androidx.compose.foundation.layout.width
22+
import androidx.compose.foundation.pager.PagerState
23+
import androidx.compose.material3.MaterialTheme
24+
import androidx.compose.runtime.Composable
25+
import androidx.compose.ui.Modifier
26+
import androidx.compose.ui.geometry.Offset
27+
import androidx.compose.ui.graphics.Color
28+
import androidx.compose.ui.graphics.lerp
29+
import androidx.compose.ui.platform.LocalDensity
30+
import androidx.compose.ui.unit.Dp
31+
import androidx.compose.ui.unit.dp
32+
import com.tunjid.heron.ui.UiTokens.withDim
33+
import kotlin.math.absoluteValue
34+
35+
@Composable
36+
fun Indicator(
37+
modifier: Modifier = Modifier,
38+
pagerState: PagerState,
39+
activeColor: Color = MaterialTheme.colorScheme.primary,
40+
inactiveColor: Color = MaterialTheme.colorScheme.primary.withDim(true),
41+
indicatorSize: Dp = 6.dp,
42+
spacing: Dp = 6.dp,
43+
maxVisibleDots: Int = 5,
44+
) {
45+
val pageCount = pagerState.pageCount
46+
if (pageCount <= 1) return
47+
48+
val density = LocalDensity.current
49+
val indicatorSizePx = with(density) { indicatorSize.toPx() }
50+
val spacingPx = with(density) { spacing.toPx() }
51+
val stepPx = indicatorSizePx + spacingPx
52+
53+
val totalWidth = (maxVisibleDots * stepPx) - spacingPx
54+
55+
Canvas(
56+
modifier = modifier
57+
.width(with(density) { totalWidth.toDp() })
58+
.height(indicatorSize),
59+
) {
60+
val currentPage = pagerState.currentPage
61+
val offset = pagerState.currentPageOffsetFraction
62+
val currentPos = currentPage + offset
63+
64+
// Calculate the center of the visible window
65+
val viewCenter = if (pageCount <= maxVisibleDots) {
66+
(pageCount - 1) / 2f
67+
} else {
68+
// Clamp the view center so we don't scroll past the start or end
69+
val lowerBound = (maxVisibleDots / 2).toFloat()
70+
val upperBound = (pageCount - 1 - (maxVisibleDots / 2)).toFloat()
71+
currentPos.coerceIn(lowerBound, upperBound)
72+
}
73+
74+
val canvasCenter = size.width / 2f
75+
76+
for (i in 0 until pageCount) {
77+
// Calculate visual position relative to canvas center
78+
val distFromViewCenter = i - viewCenter
79+
val x = canvasCenter + distFromViewCenter * stepPx
80+
81+
// Skip if far out of view
82+
if (x < -stepPx || x > size.width + stepPx) continue
83+
84+
// Calculate scale based on distance from selection (Active vs Inactive)
85+
val distFromSelection = (i - currentPos).absoluteValue
86+
87+
// Active dot is 1.2x bigger than inactive (1.0x).
88+
// Scale changes linearly with offset.
89+
var scale = if (distFromSelection < 1f) {
90+
1.2f - 0.2f * distFromSelection
91+
} else {
92+
1.0f
93+
}
94+
95+
// Edge scaling for shifting effect
96+
if (pageCount > maxVisibleDots) {
97+
val distFromView = (i - viewCenter).absoluteValue
98+
val halfVisible = maxVisibleDots / 2f
99+
100+
// Scale down dots as they approach the edge of the visible window
101+
val edgeDist = distFromView - (halfVisible - 1f)
102+
if (edgeDist > 0) {
103+
val edgeScale = 1f - 0.5f * edgeDist.coerceAtMost(1f)
104+
scale *= edgeScale
105+
}
106+
}
107+
108+
val radius = (indicatorSizePx / 2f) * scale
109+
110+
val color = if (distFromSelection < 1f) {
111+
lerp(activeColor, inactiveColor, distFromSelection)
112+
} else {
113+
inactiveColor
114+
}
115+
116+
drawCircle(
117+
color = color,
118+
radius = radius,
119+
center = Offset(x, size.height / 2f),
120+
)
121+
}
122+
}
123+
}

0 commit comments

Comments
 (0)