Skip to content

Commit da9fe06

Browse files
committed
Use a vertical carousel in detail page in star sample
Resolves #117, also slightly better interaction
1 parent d4085f0 commit da9fe06

File tree

1 file changed

+103
-48
lines changed

1 file changed

+103
-48
lines changed

samples/star/src/main/kotlin/com/slack/circuit/star/petdetail/PetPhotoCarousel.kt

Lines changed: 103 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,22 @@
22
// SPDX-License-Identifier: Apache-2.0
33
package com.slack.circuit.star.petdetail
44

5+
import android.content.res.Configuration
56
import android.view.KeyEvent
67
import androidx.compose.animation.animateContentSize
78
import androidx.compose.animation.core.AnimationConstants
89
import androidx.compose.foundation.ExperimentalFoundationApi
910
import androidx.compose.foundation.focusable
1011
import androidx.compose.foundation.layout.Column
12+
import androidx.compose.foundation.layout.ColumnScope
1113
import androidx.compose.foundation.layout.PaddingValues
1214
import androidx.compose.foundation.layout.aspectRatio
1315
import androidx.compose.foundation.layout.fillMaxSize
1416
import androidx.compose.foundation.layout.fillMaxWidth
1517
import androidx.compose.foundation.layout.padding
1618
import androidx.compose.foundation.pager.HorizontalPager
1719
import androidx.compose.foundation.pager.PagerState
20+
import androidx.compose.foundation.pager.VerticalPager
1821
import androidx.compose.foundation.pager.rememberPagerState
1922
import androidx.compose.material3.Card
2023
import androidx.compose.material3.MaterialTheme
@@ -29,6 +32,7 @@ import androidx.compose.ui.focus.focusRequester
2932
import androidx.compose.ui.graphics.graphicsLayer
3033
import androidx.compose.ui.input.key.onKeyEvent
3134
import androidx.compose.ui.layout.ContentScale
35+
import androidx.compose.ui.platform.LocalConfiguration
3236
import androidx.compose.ui.platform.LocalContext
3337
import androidx.compose.ui.platform.testTag
3438
import androidx.compose.ui.unit.dp
@@ -37,6 +41,7 @@ import coil.compose.AsyncImage
3741
import coil.imageLoader
3842
import coil.request.ImageRequest
3943
import com.google.accompanist.pager.HorizontalPagerIndicator
44+
import com.google.accompanist.pager.VerticalPagerIndicator
4045
import com.slack.circuit.codegen.annotations.CircuitInject
4146
import com.slack.circuit.runtime.CircuitUiState
4247
import com.slack.circuit.runtime.Screen
@@ -145,6 +150,7 @@ internal fun PetPhotoCarousel(state: PetPhotoCarouselScreen.State, modifier: Mod
145150
.focusable()
146151
.onKeyEvent { event ->
147152
if (event.nativeKeyEvent.action != KeyEvent.ACTION_UP) return@onKeyEvent false
153+
// TODO vert
148154
val index =
149155
when (event.nativeKeyEvent.keyCode) {
150156
KeyEvent.KEYCODE_DPAD_RIGHT -> {
@@ -163,20 +169,26 @@ internal fun PetPhotoCarousel(state: PetPhotoCarouselScreen.State, modifier: Mod
163169
}
164170
}
165171
) {
166-
PhotoPager(
167-
count = totalPhotos,
168-
pagerState = pagerState,
169-
photoUrls = photoUrls,
170-
name = name,
171-
photoUrlMemoryCacheKey = photoUrlMemoryCacheKey,
172-
)
173-
174-
HorizontalPagerIndicator(
175-
pagerState = pagerState,
176-
pageCount = totalPhotos,
177-
modifier = Modifier.align(Alignment.CenterHorizontally).padding(16.dp),
178-
activeColor = MaterialTheme.colorScheme.onBackground
179-
)
172+
when (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) {
173+
true -> {
174+
VerticalPhotoPager(
175+
count = totalPhotos,
176+
pagerState = pagerState,
177+
photoUrls = photoUrls,
178+
name = name,
179+
photoUrlMemoryCacheKey = photoUrlMemoryCacheKey,
180+
)
181+
}
182+
false -> {
183+
HorizontalPhotoPager(
184+
count = totalPhotos,
185+
pagerState = pagerState,
186+
photoUrls = photoUrls,
187+
name = name,
188+
photoUrlMemoryCacheKey = photoUrlMemoryCacheKey,
189+
)
190+
}
191+
}
180192
}
181193

182194
// Focus the pager so we can cycle through it with arrow keys
@@ -188,10 +200,9 @@ private fun PagerState.calculateCurrentOffsetForPage(page: Int): Float {
188200
return (currentPage - page) + currentPageOffsetFraction
189201
}
190202

191-
@Suppress("LongParameterList")
192203
@OptIn(ExperimentalFoundationApi::class)
193204
@Composable
194-
private fun PhotoPager(
205+
private fun ColumnScope.HorizontalPhotoPager(
195206
count: Int,
196207
pagerState: PagerState,
197208
photoUrls: ImmutableList<String>,
@@ -206,40 +217,84 @@ private fun PhotoPager(
206217
modifier = modifier,
207218
contentPadding = PaddingValues(16.dp),
208219
) { page ->
209-
Card(
210-
modifier =
211-
Modifier.aspectRatio(1f).graphicsLayer {
212-
// Calculate the absolute offset for the current page from the
213-
// scroll position. We use the absolute value which allows us to mirror
214-
// any effects for both directions
215-
val pageOffset = pagerState.calculateCurrentOffsetForPage(page).absoluteValue
216-
217-
// We animate the scaleX + scaleY, between 85% and 100%
218-
lerp(start = 0.85f, stop = 1f, fraction = 1f - pageOffset.coerceIn(0f, 1f)).also { scale
219-
->
220-
scaleX = scale
221-
scaleY = scale
222-
}
220+
PhotoPage(page, pagerState, photoUrls, name, photoUrlMemoryCacheKey)
221+
}
223222

224-
// We animate the alpha, between 50% and 100%
225-
alpha = lerp(start = 0.5f, stop = 1f, fraction = 1f - pageOffset.coerceIn(0f, 1f))
223+
HorizontalPagerIndicator(
224+
pagerState = pagerState,
225+
pageCount = count,
226+
modifier = Modifier.align(Alignment.CenterHorizontally).padding(16.dp),
227+
activeColor = MaterialTheme.colorScheme.onBackground
228+
)
229+
}
230+
231+
@OptIn(ExperimentalFoundationApi::class)
232+
@Composable
233+
private fun ColumnScope.VerticalPhotoPager(
234+
count: Int,
235+
pagerState: PagerState,
236+
photoUrls: ImmutableList<String>,
237+
name: String,
238+
photoUrlMemoryCacheKey: String? = null,
239+
) {
240+
VerticalPager(
241+
pageCount = count,
242+
state = pagerState,
243+
key = photoUrls::get,
244+
contentPadding = PaddingValues(16.dp),
245+
) { page ->
246+
PhotoPage(page, pagerState, photoUrls, name, photoUrlMemoryCacheKey)
247+
}
248+
249+
VerticalPagerIndicator(
250+
pagerState = pagerState,
251+
pageCount = count,
252+
modifier = Modifier.align(Alignment.CenterHorizontally).padding(16.dp),
253+
activeColor = MaterialTheme.colorScheme.onBackground
254+
)
255+
}
256+
257+
@OptIn(ExperimentalFoundationApi::class)
258+
@Composable
259+
private fun PhotoPage(
260+
page: Int,
261+
pagerState: PagerState,
262+
photoUrls: ImmutableList<String>,
263+
name: String,
264+
photoUrlMemoryCacheKey: String? = null,
265+
) {
266+
Card(
267+
modifier =
268+
Modifier.aspectRatio(1f).graphicsLayer {
269+
// Calculate the absolute offset for the current page from the
270+
// scroll position. We use the absolute value which allows us to mirror
271+
// any effects for both directions
272+
val pageOffset = pagerState.calculateCurrentOffsetForPage(page).absoluteValue
273+
274+
// We animate the scaleX + scaleY, between 85% and 100%
275+
lerp(start = 0.85f, stop = 1f, fraction = 1f - pageOffset.coerceIn(0f, 1f)).also { scale ->
276+
scaleX = scale
277+
scaleY = scale
226278
}
227-
) {
228-
AsyncImage(
229-
modifier = Modifier.fillMaxWidth(),
230-
model =
231-
ImageRequest.Builder(LocalContext.current)
232-
.data(photoUrls[page].takeIf(String::isNotBlank))
233-
.apply {
234-
if (page == 0) {
235-
placeholderMemoryCacheKey(photoUrlMemoryCacheKey)
236-
crossfade(AnimationConstants.DefaultDurationMillis)
237-
}
279+
280+
// We animate the alpha, between 50% and 100%
281+
alpha = lerp(start = 0.5f, stop = 1f, fraction = 1f - pageOffset.coerceIn(0f, 1f))
282+
}
283+
) {
284+
AsyncImage(
285+
modifier = Modifier.fillMaxWidth(),
286+
model =
287+
ImageRequest.Builder(LocalContext.current)
288+
.data(photoUrls[page].takeIf(String::isNotBlank))
289+
.apply {
290+
if (page == 0) {
291+
placeholderMemoryCacheKey(photoUrlMemoryCacheKey)
292+
crossfade(AnimationConstants.DefaultDurationMillis)
238293
}
239-
.build(),
240-
contentDescription = name,
241-
contentScale = ContentScale.Crop,
242-
)
243-
}
294+
}
295+
.build(),
296+
contentDescription = name,
297+
contentScale = ContentScale.Crop,
298+
)
244299
}
245300
}

0 commit comments

Comments
 (0)