mirror of
https://github.com/jlengrand/compose-multiplatform.git
synced 2026-03-10 08:11:20 +00:00
ImageViewer, fix Android rotation (#3007)
This commit is contained in:
@@ -6,6 +6,7 @@ plugins {
|
||||
id("com.android.library")
|
||||
id("org.jetbrains.compose")
|
||||
kotlin("plugin.serialization")
|
||||
id("kotlin-parcelize")
|
||||
}
|
||||
|
||||
version = "1.0-SNAPSHOT"
|
||||
|
||||
@@ -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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<PictureData>
|
||||
) {
|
||||
val selectedPictureIndex = remember { mutableStateOf(0) }
|
||||
val navigationStack = remember { NavigationStack<Page>(GalleryPage()) }
|
||||
// rememberSaveable is required to properly handle Android configuration changes (such as device rotation)
|
||||
val selectedPictureIndex = rememberSaveable { mutableStateOf(0) }
|
||||
val navigationStack = rememberSaveable(
|
||||
saver = listSaver<NavigationStack<Page>, 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))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -1,10 +0,0 @@
|
||||
package example.imageviewer.model
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
|
||||
sealed interface Page
|
||||
|
||||
class MemoryPage(val pictureState: MutableState<PictureData>) : Page
|
||||
class CameraPage : Page
|
||||
class FullScreenPage(val picture: PictureData) : Page
|
||||
class GalleryPage : Page
|
||||
@@ -50,7 +50,7 @@ enum class GalleryStyle {
|
||||
fun GalleryScreen(
|
||||
pictures: SnapshotStateList<PictureData>,
|
||||
selectedPictureIndex: MutableState<Int>,
|
||||
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<PictureData>,
|
||||
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<PictureData>,
|
||||
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)
|
||||
|
||||
@@ -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<PictureData>,
|
||||
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<PictureData>,
|
||||
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) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,18 +2,22 @@ package example.imageviewer.view
|
||||
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
|
||||
class NavigationStack<T>(initial: T) {
|
||||
private val stack = mutableStateListOf(initial)
|
||||
class NavigationStack<T>(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()
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user