diff --git a/examples/imageviewer/shared/build.gradle.kts b/examples/imageviewer/shared/build.gradle.kts index c672a729fe..2cff70a368 100755 --- a/examples/imageviewer/shared/build.gradle.kts +++ b/examples/imageviewer/shared/build.gradle.kts @@ -6,6 +6,7 @@ plugins { id("com.android.library") id("org.jetbrains.compose") kotlin("plugin.serialization") + id("kotlin-parcelize") } version = "1.0-SNAPSHOT" diff --git a/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/model/Page.android.kt b/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/model/Page.android.kt new file mode 100644 index 0000000000..3dfc97c6bd --- /dev/null +++ b/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/model/Page.android.kt @@ -0,0 +1,16 @@ +package example.imageviewer.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +actual class MemoryPage actual constructor(actual val pictureIndex: Int) : Page, Parcelable + +@Parcelize +actual class CameraPage : Page, Parcelable + +@Parcelize +actual class FullScreenPage actual constructor(actual val pictureIndex: Int) : Page, Parcelable + +@Parcelize +actual class GalleryPage : Page, Parcelable diff --git a/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/CameraView.android.kt b/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/CameraView.android.kt index 99244b8d23..4f10e50b89 100644 --- a/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/CameraView.android.kt +++ b/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/CameraView.android.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.CircularProgressIndicator import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -82,7 +83,7 @@ private fun CameraWithGrantedPermission( val preview = Preview.Builder().build() val previewView = remember { PreviewView(context) } val imageCapture: ImageCapture = remember { ImageCapture.Builder().build() } - var isFrontCamera by remember { mutableStateOf(false) } + var isFrontCamera by rememberSaveable { mutableStateOf(false) } val cameraSelector = remember(isFrontCamera) { val lensFacing = if (isFrontCamera) { diff --git a/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/ImageViewer.common.kt b/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/ImageViewer.common.kt index fe1b0179c6..3901bc53e7 100644 --- a/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/ImageViewer.common.kt +++ b/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/ImageViewer.common.kt @@ -3,6 +3,8 @@ package example.imageviewer import androidx.compose.animation.* import androidx.compose.animation.core.tween import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.listSaver +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.snapshots.SnapshotStateList import example.imageviewer.model.* import example.imageviewer.view.* @@ -33,8 +35,17 @@ fun ImageViewerCommon( fun ImageViewerWithProvidedDependencies( pictures: SnapshotStateList ) { - val selectedPictureIndex = remember { mutableStateOf(0) } - val navigationStack = remember { NavigationStack(GalleryPage()) } + // rememberSaveable is required to properly handle Android configuration changes (such as device rotation) + val selectedPictureIndex = rememberSaveable { mutableStateOf(0) } + val navigationStack = rememberSaveable( + saver = listSaver, Page>( + restore = { NavigationStack(*it.toTypedArray()) }, + save = { it.stack }, + ) + ) { + NavigationStack(GalleryPage()) + } + val externalEvents = LocalInternalEvents.current LaunchedEffect(Unit) { externalEvents.collect { @@ -62,8 +73,8 @@ fun ImageViewerWithProvidedDependencies( GalleryScreen( pictures = pictures, selectedPictureIndex = selectedPictureIndex, - onClickPreviewPicture = { previewPictureId -> - navigationStack.push(MemoryPage(mutableStateOf(previewPictureId))) + onClickPreviewPicture = { previewPictureIndex -> + navigationStack.push(MemoryPage(previewPictureIndex)) } ) { navigationStack.push(CameraPage()) @@ -72,7 +83,7 @@ fun ImageViewerWithProvidedDependencies( is FullScreenPage -> { FullscreenImageScreen( - picture = page.picture, + picture = pictures[page.pictureIndex], back = { navigationStack.back() } @@ -83,14 +94,19 @@ fun ImageViewerWithProvidedDependencies( MemoryScreen( pictures = pictures, memoryPage = page, - onSelectRelatedMemory = { picture -> - navigationStack.push(MemoryPage(mutableStateOf(picture))) + onSelectRelatedMemory = { pictureIndex -> + navigationStack.push(MemoryPage(pictureIndex)) }, - onBack = { - navigationStack.back() + onBack = { resetNavigation -> + if (resetNavigation) { + selectedPictureIndex.value = 0 + navigationStack.reset() + } else { + navigationStack.back() + } }, - onHeaderClick = { galleryId -> - navigationStack.push(FullScreenPage(galleryId)) + onHeaderClick = { pictureIndex -> + navigationStack.push(FullScreenPage(pictureIndex)) }, ) } diff --git a/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/Page.common.kt b/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/Page.common.kt new file mode 100644 index 0000000000..b2a0abc2de --- /dev/null +++ b/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/Page.common.kt @@ -0,0 +1,15 @@ +package example.imageviewer.model + +interface Page + +expect class MemoryPage(pictureIndex: Int) : Page { + val pictureIndex: Int +} + +expect class CameraPage() : Page + +expect class FullScreenPage(pictureIndex: Int) : Page { + val pictureIndex: Int +} + +expect class GalleryPage() : Page diff --git a/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/Page.kt b/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/Page.kt deleted file mode 100644 index 6b57aa0362..0000000000 --- a/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/Page.kt +++ /dev/null @@ -1,10 +0,0 @@ -package example.imageviewer.model - -import androidx.compose.runtime.MutableState - -sealed interface Page - -class MemoryPage(val pictureState: MutableState) : Page -class CameraPage : Page -class FullScreenPage(val picture: PictureData) : Page -class GalleryPage : Page diff --git a/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/GalleryScreen.kt b/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/GalleryScreen.kt index 3dc6caf626..a161f5ee93 100755 --- a/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/GalleryScreen.kt +++ b/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/GalleryScreen.kt @@ -50,7 +50,7 @@ enum class GalleryStyle { fun GalleryScreen( pictures: SnapshotStateList, selectedPictureIndex: MutableState, - onClickPreviewPicture: (PictureData) -> Unit, + onClickPreviewPicture: (index: Int) -> Unit, onMakeNewMemory: () -> Unit ) { val imageProvider = LocalImageProvider.current @@ -113,17 +113,17 @@ fun GalleryScreen( Box( Modifier.fillMaxSize() .clickable { - onClickPreviewPicture(pictures[pagerState.currentPage]) + onClickPreviewPicture(pagerState.currentPage) } ) { - HorizontalPager(pictures.size, state = pagerState) { idx -> - val picture = pictures[idx] + HorizontalPager(pictures.size, state = pagerState) { index -> + val picture = pictures[index] var image: ImageBitmap? by remember(picture) { mutableStateOf(null) } LaunchedEffect(picture) { image = imageProvider.getImage(picture) } if (image != null) { - Box(Modifier.fillMaxSize().animatePageChanges(pagerState, idx)) { + Box(Modifier.fillMaxSize().animatePageChanges(pagerState, index)) { Image( bitmap = image!!, contentDescription = null, @@ -176,7 +176,7 @@ fun GalleryScreen( private fun SquaresGalleryView( images: List, pagerState: PagerState, - onSelect: (Int) -> Unit, + onSelect: (index: Int) -> Unit, ) { LazyVerticalGrid( modifier = Modifier.padding(top = 4.dp), @@ -184,11 +184,11 @@ private fun SquaresGalleryView( verticalArrangement = Arrangement.spacedBy(1.dp), horizontalArrangement = Arrangement.spacedBy(1.dp) ) { - itemsIndexed(images) { idx, picture -> + itemsIndexed(images) { index, picture -> SquareThumbnail( picture = picture, - onClick = { onSelect(idx) }, - isHighlighted = pagerState.targetPage == idx + onClick = { onSelect(index) }, + isHighlighted = pagerState.targetPage == index ) } } @@ -244,8 +244,8 @@ fun SquareThumbnail( @Composable private fun ListGalleryView( pictures: List, - onSelect: (Int) -> Unit, - onFullScreen: (PictureData) -> Unit, + onSelect: (index: Int) -> Unit, + onFullScreen: (index: Int) -> Unit, ) { val notification = LocalNotification.current ScrollableColumn( @@ -259,7 +259,7 @@ private fun ListGalleryView( onSelect(p.index) }, onClickFullScreen = { - onFullScreen(p.value) + onFullScreen(p.index) }, onClickInfo = { notification.notifyImageData(p.value) diff --git a/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/MemoryScreen.kt b/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/MemoryScreen.kt index 3a9c244f96..4c52590935 100644 --- a/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/MemoryScreen.kt +++ b/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/MemoryScreen.kt @@ -38,20 +38,19 @@ import example.imageviewer.model.* import example.imageviewer.shareIcon import example.imageviewer.style.ImageviewerColors import org.jetbrains.compose.resources.ExperimentalResourceApi -import org.jetbrains.compose.resources.painterResource @Composable fun MemoryScreen( pictures: SnapshotStateList, memoryPage: MemoryPage, - onSelectRelatedMemory: (PictureData) -> Unit, - onBack: () -> Unit, - onHeaderClick: (PictureData) -> Unit, + onSelectRelatedMemory: (index: Int) -> Unit, + onBack: (resetNavigation: Boolean) -> Unit, + onHeaderClick: (index: Int) -> Unit, ) { val imageProvider = LocalImageProvider.current val sharePicture = LocalSharePicture.current var edit: Boolean by remember { mutableStateOf(false) } - val picture = memoryPage.pictureState.value + val picture = pictures.getOrNull(memoryPage.pictureIndex) ?: return var headerImage: ImageBitmap? by remember(picture) { mutableStateOf(null) } val platformContext = getPlatformContext() LaunchedEffect(picture) { @@ -78,7 +77,7 @@ fun MemoryScreen( MemoryHeader( it, picture = picture, - onClick = { onHeaderClick(picture) } + onClick = { onHeaderClick(memoryPage.pictureIndex) } ) } } @@ -106,7 +105,7 @@ fun MemoryScreen( Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) { IconWithText(Icons.Default.Delete, "Delete") { imageProvider.delete(picture) - onBack() + onBack(true) } IconWithText(Icons.Default.Edit, "Edit") { edit = true @@ -123,14 +122,15 @@ fun MemoryScreen( } TopLayout( alignLeftContent = { - BackButton(onBack) + BackButton { + onBack(false) + } }, alignRightContent = {}, ) if (edit) { EditMemoryDialog(picture.name, picture.description) { name, description -> - val edited = imageProvider.edit(picture, name, description) - memoryPage.pictureState.value = edited + imageProvider.edit(picture, name, description) edit = false } } @@ -267,7 +267,7 @@ fun Headliner(s: String) { @Composable fun RelatedMemoriesVisualizer( pictures: List, - onSelectRelatedMemory: (PictureData) -> Unit + onSelectRelatedMemory: (index: Int) -> Unit ) { Box( modifier = Modifier.padding(10.dp, 0.dp).clip(RoundedCornerShape(10.dp)).fillMaxWidth() @@ -276,22 +276,14 @@ fun RelatedMemoriesVisualizer( modifier = Modifier.fillMaxSize(), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - itemsIndexed(pictures) { idx, item -> - RelatedMemory(item, onSelectRelatedMemory) + itemsIndexed(pictures) { index, item -> + Box(Modifier.size(130.dp).clip(RoundedCornerShape(8.dp))) { + SquareThumbnail( + picture = item, + isHighlighted = false, + onClick = { onSelectRelatedMemory(index) }) + } } } } } - -@Composable -fun RelatedMemory( - galleryEntry: PictureData, - onSelectRelatedMemory: (PictureData) -> Unit -) { - Box(Modifier.size(130.dp).clip(RoundedCornerShape(8.dp))) { - SquareThumbnail( - picture = galleryEntry, - isHighlighted = false, - onClick = { onSelectRelatedMemory(galleryEntry) }) - } -} diff --git a/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/NavigationStack.kt b/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/NavigationStack.kt index 1f9f4a7708..6eed9b9767 100644 --- a/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/NavigationStack.kt +++ b/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/NavigationStack.kt @@ -2,18 +2,22 @@ package example.imageviewer.view import androidx.compose.runtime.mutableStateListOf -class NavigationStack(initial: T) { - private val stack = mutableStateListOf(initial) +class NavigationStack(vararg initial: T) { + val stack = mutableStateListOf(*initial) fun push(t: T) { stack.add(t) } fun back() { - if(stack.size > 1) { + if (stack.size > 1) { // Always keep one element on the view stack stack.removeLast() } } + fun reset() { + stack.removeRange(1, stack.size) + } + fun lastWithIndex() = stack.withIndex().last() } \ No newline at end of file diff --git a/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/model/Page.desktop.kt b/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/model/Page.desktop.kt new file mode 100644 index 0000000000..da2b29c61d --- /dev/null +++ b/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/model/Page.desktop.kt @@ -0,0 +1,9 @@ +package example.imageviewer.model + +actual class MemoryPage actual constructor(actual val pictureIndex: Int) : Page + +actual class CameraPage : Page + +actual class FullScreenPage actual constructor(actual val pictureIndex: Int) : Page + +actual class GalleryPage : Page diff --git a/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/model/Page.ios.kt b/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/model/Page.ios.kt new file mode 100644 index 0000000000..da2b29c61d --- /dev/null +++ b/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/model/Page.ios.kt @@ -0,0 +1,9 @@ +package example.imageviewer.model + +actual class MemoryPage actual constructor(actual val pictureIndex: Int) : Page + +actual class CameraPage : Page + +actual class FullScreenPage actual constructor(actual val pictureIndex: Int) : Page + +actual class GalleryPage : Page