Browse Source

ImageViewer, fix Android rotation (#3007)

pull/3009/head
dima.avdeev 2 years ago committed by GitHub
parent
commit
523368228b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      examples/imageviewer/shared/build.gradle.kts
  2. 16
      examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/model/Page.android.kt
  3. 3
      examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/CameraView.android.kt
  4. 38
      examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/ImageViewer.common.kt
  5. 15
      examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/Page.common.kt
  6. 10
      examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/Page.kt
  7. 24
      examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/GalleryScreen.kt
  8. 44
      examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/MemoryScreen.kt
  9. 10
      examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/NavigationStack.kt
  10. 9
      examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/model/Page.desktop.kt
  11. 9
      examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/model/Page.ios.kt

1
examples/imageviewer/shared/build.gradle.kts

@ -6,6 +6,7 @@ plugins {
id("com.android.library") id("com.android.library")
id("org.jetbrains.compose") id("org.jetbrains.compose")
kotlin("plugin.serialization") kotlin("plugin.serialization")
id("kotlin-parcelize")
} }
version = "1.0-SNAPSHOT" version = "1.0-SNAPSHOT"

16
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

3
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.foundation.layout.size
import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.CircularProgressIndicator
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@ -82,7 +83,7 @@ private fun CameraWithGrantedPermission(
val preview = Preview.Builder().build() val preview = Preview.Builder().build()
val previewView = remember { PreviewView(context) } val previewView = remember { PreviewView(context) }
val imageCapture: ImageCapture = remember { ImageCapture.Builder().build() } val imageCapture: ImageCapture = remember { ImageCapture.Builder().build() }
var isFrontCamera by remember { mutableStateOf(false) } var isFrontCamera by rememberSaveable { mutableStateOf(false) }
val cameraSelector = remember(isFrontCamera) { val cameraSelector = remember(isFrontCamera) {
val lensFacing = val lensFacing =
if (isFrontCamera) { if (isFrontCamera) {

38
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.*
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.listSaver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.snapshots.SnapshotStateList
import example.imageviewer.model.* import example.imageviewer.model.*
import example.imageviewer.view.* import example.imageviewer.view.*
@ -33,8 +35,17 @@ fun ImageViewerCommon(
fun ImageViewerWithProvidedDependencies( fun ImageViewerWithProvidedDependencies(
pictures: SnapshotStateList<PictureData> pictures: SnapshotStateList<PictureData>
) { ) {
val selectedPictureIndex = remember { mutableStateOf(0) } // rememberSaveable is required to properly handle Android configuration changes (such as device rotation)
val navigationStack = remember { NavigationStack<Page>(GalleryPage()) } 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 val externalEvents = LocalInternalEvents.current
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
externalEvents.collect { externalEvents.collect {
@ -62,8 +73,8 @@ fun ImageViewerWithProvidedDependencies(
GalleryScreen( GalleryScreen(
pictures = pictures, pictures = pictures,
selectedPictureIndex = selectedPictureIndex, selectedPictureIndex = selectedPictureIndex,
onClickPreviewPicture = { previewPictureId -> onClickPreviewPicture = { previewPictureIndex ->
navigationStack.push(MemoryPage(mutableStateOf(previewPictureId))) navigationStack.push(MemoryPage(previewPictureIndex))
} }
) { ) {
navigationStack.push(CameraPage()) navigationStack.push(CameraPage())
@ -72,7 +83,7 @@ fun ImageViewerWithProvidedDependencies(
is FullScreenPage -> { is FullScreenPage -> {
FullscreenImageScreen( FullscreenImageScreen(
picture = page.picture, picture = pictures[page.pictureIndex],
back = { back = {
navigationStack.back() navigationStack.back()
} }
@ -83,14 +94,19 @@ fun ImageViewerWithProvidedDependencies(
MemoryScreen( MemoryScreen(
pictures = pictures, pictures = pictures,
memoryPage = page, memoryPage = page,
onSelectRelatedMemory = { picture -> onSelectRelatedMemory = { pictureIndex ->
navigationStack.push(MemoryPage(mutableStateOf(picture))) navigationStack.push(MemoryPage(pictureIndex))
}, },
onBack = { onBack = { resetNavigation ->
navigationStack.back() if (resetNavigation) {
selectedPictureIndex.value = 0
navigationStack.reset()
} else {
navigationStack.back()
}
}, },
onHeaderClick = { galleryId -> onHeaderClick = { pictureIndex ->
navigationStack.push(FullScreenPage(galleryId)) navigationStack.push(FullScreenPage(pictureIndex))
}, },
) )
} }

15
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

10
examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/Page.kt

@ -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

24
examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/GalleryScreen.kt

@ -50,7 +50,7 @@ enum class GalleryStyle {
fun GalleryScreen( fun GalleryScreen(
pictures: SnapshotStateList<PictureData>, pictures: SnapshotStateList<PictureData>,
selectedPictureIndex: MutableState<Int>, selectedPictureIndex: MutableState<Int>,
onClickPreviewPicture: (PictureData) -> Unit, onClickPreviewPicture: (index: Int) -> Unit,
onMakeNewMemory: () -> Unit onMakeNewMemory: () -> Unit
) { ) {
val imageProvider = LocalImageProvider.current val imageProvider = LocalImageProvider.current
@ -113,17 +113,17 @@ fun GalleryScreen(
Box( Box(
Modifier.fillMaxSize() Modifier.fillMaxSize()
.clickable { .clickable {
onClickPreviewPicture(pictures[pagerState.currentPage]) onClickPreviewPicture(pagerState.currentPage)
} }
) { ) {
HorizontalPager(pictures.size, state = pagerState) { idx -> HorizontalPager(pictures.size, state = pagerState) { index ->
val picture = pictures[idx] val picture = pictures[index]
var image: ImageBitmap? by remember(picture) { mutableStateOf(null) } var image: ImageBitmap? by remember(picture) { mutableStateOf(null) }
LaunchedEffect(picture) { LaunchedEffect(picture) {
image = imageProvider.getImage(picture) image = imageProvider.getImage(picture)
} }
if (image != null) { if (image != null) {
Box(Modifier.fillMaxSize().animatePageChanges(pagerState, idx)) { Box(Modifier.fillMaxSize().animatePageChanges(pagerState, index)) {
Image( Image(
bitmap = image!!, bitmap = image!!,
contentDescription = null, contentDescription = null,
@ -176,7 +176,7 @@ fun GalleryScreen(
private fun SquaresGalleryView( private fun SquaresGalleryView(
images: List<PictureData>, images: List<PictureData>,
pagerState: PagerState, pagerState: PagerState,
onSelect: (Int) -> Unit, onSelect: (index: Int) -> Unit,
) { ) {
LazyVerticalGrid( LazyVerticalGrid(
modifier = Modifier.padding(top = 4.dp), modifier = Modifier.padding(top = 4.dp),
@ -184,11 +184,11 @@ private fun SquaresGalleryView(
verticalArrangement = Arrangement.spacedBy(1.dp), verticalArrangement = Arrangement.spacedBy(1.dp),
horizontalArrangement = Arrangement.spacedBy(1.dp) horizontalArrangement = Arrangement.spacedBy(1.dp)
) { ) {
itemsIndexed(images) { idx, picture -> itemsIndexed(images) { index, picture ->
SquareThumbnail( SquareThumbnail(
picture = picture, picture = picture,
onClick = { onSelect(idx) }, onClick = { onSelect(index) },
isHighlighted = pagerState.targetPage == idx isHighlighted = pagerState.targetPage == index
) )
} }
} }
@ -244,8 +244,8 @@ fun SquareThumbnail(
@Composable @Composable
private fun ListGalleryView( private fun ListGalleryView(
pictures: List<PictureData>, pictures: List<PictureData>,
onSelect: (Int) -> Unit, onSelect: (index: Int) -> Unit,
onFullScreen: (PictureData) -> Unit, onFullScreen: (index: Int) -> Unit,
) { ) {
val notification = LocalNotification.current val notification = LocalNotification.current
ScrollableColumn( ScrollableColumn(
@ -259,7 +259,7 @@ private fun ListGalleryView(
onSelect(p.index) onSelect(p.index)
}, },
onClickFullScreen = { onClickFullScreen = {
onFullScreen(p.value) onFullScreen(p.index)
}, },
onClickInfo = { onClickInfo = {
notification.notifyImageData(p.value) notification.notifyImageData(p.value)

44
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.shareIcon
import example.imageviewer.style.ImageviewerColors import example.imageviewer.style.ImageviewerColors
import org.jetbrains.compose.resources.ExperimentalResourceApi import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.painterResource
@Composable @Composable
fun MemoryScreen( fun MemoryScreen(
pictures: SnapshotStateList<PictureData>, pictures: SnapshotStateList<PictureData>,
memoryPage: MemoryPage, memoryPage: MemoryPage,
onSelectRelatedMemory: (PictureData) -> Unit, onSelectRelatedMemory: (index: Int) -> Unit,
onBack: () -> Unit, onBack: (resetNavigation: Boolean) -> Unit,
onHeaderClick: (PictureData) -> Unit, onHeaderClick: (index: Int) -> Unit,
) { ) {
val imageProvider = LocalImageProvider.current val imageProvider = LocalImageProvider.current
val sharePicture = LocalSharePicture.current val sharePicture = LocalSharePicture.current
var edit: Boolean by remember { mutableStateOf(false) } 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) } var headerImage: ImageBitmap? by remember(picture) { mutableStateOf(null) }
val platformContext = getPlatformContext() val platformContext = getPlatformContext()
LaunchedEffect(picture) { LaunchedEffect(picture) {
@ -78,7 +77,7 @@ fun MemoryScreen(
MemoryHeader( MemoryHeader(
it, it,
picture = picture, picture = picture,
onClick = { onHeaderClick(picture) } onClick = { onHeaderClick(memoryPage.pictureIndex) }
) )
} }
} }
@ -106,7 +105,7 @@ fun MemoryScreen(
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) { Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
IconWithText(Icons.Default.Delete, "Delete") { IconWithText(Icons.Default.Delete, "Delete") {
imageProvider.delete(picture) imageProvider.delete(picture)
onBack() onBack(true)
} }
IconWithText(Icons.Default.Edit, "Edit") { IconWithText(Icons.Default.Edit, "Edit") {
edit = true edit = true
@ -123,14 +122,15 @@ fun MemoryScreen(
} }
TopLayout( TopLayout(
alignLeftContent = { alignLeftContent = {
BackButton(onBack) BackButton {
onBack(false)
}
}, },
alignRightContent = {}, alignRightContent = {},
) )
if (edit) { if (edit) {
EditMemoryDialog(picture.name, picture.description) { name, description -> EditMemoryDialog(picture.name, picture.description) { name, description ->
val edited = imageProvider.edit(picture, name, description) imageProvider.edit(picture, name, description)
memoryPage.pictureState.value = edited
edit = false edit = false
} }
} }
@ -267,7 +267,7 @@ fun Headliner(s: String) {
@Composable @Composable
fun RelatedMemoriesVisualizer( fun RelatedMemoriesVisualizer(
pictures: List<PictureData>, pictures: List<PictureData>,
onSelectRelatedMemory: (PictureData) -> Unit onSelectRelatedMemory: (index: Int) -> Unit
) { ) {
Box( Box(
modifier = Modifier.padding(10.dp, 0.dp).clip(RoundedCornerShape(10.dp)).fillMaxWidth() modifier = Modifier.padding(10.dp, 0.dp).clip(RoundedCornerShape(10.dp)).fillMaxWidth()
@ -276,22 +276,14 @@ fun RelatedMemoriesVisualizer(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
itemsIndexed(pictures) { idx, item -> itemsIndexed(pictures) { index, item ->
RelatedMemory(item, onSelectRelatedMemory) 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) })
}
}

10
examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/NavigationStack.kt

@ -2,18 +2,22 @@ package example.imageviewer.view
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
class NavigationStack<T>(initial: T) { class NavigationStack<T>(vararg initial: T) {
private val stack = mutableStateListOf(initial) val stack = mutableStateListOf(*initial)
fun push(t: T) { fun push(t: T) {
stack.add(t) stack.add(t)
} }
fun back() { fun back() {
if(stack.size > 1) { if (stack.size > 1) {
// Always keep one element on the view stack // Always keep one element on the view stack
stack.removeLast() stack.removeLast()
} }
} }
fun reset() {
stack.removeRange(1, stack.size)
}
fun lastWithIndex() = stack.withIndex().last() fun lastWithIndex() = stack.withIndex().last()
} }

9
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

9
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
Loading…
Cancel
Save