ImageViewer Pager and icons (#2982)

This commit is contained in:
dima.avdeev
2023-04-06 12:13:33 +03:00
committed by GitHub
parent 35ecc36b38
commit 968af859c3
29 changed files with 538 additions and 290 deletions

View File

@@ -35,7 +35,7 @@ kotlin {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material)
//implementation(compose.materialIconsExtended) // TODO not working on iOS
//implementation(compose.materialIconsExtended) // TODO not working on iOS for now
@OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
implementation(compose.components.resources)
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")

View File

@@ -2,14 +2,13 @@ package example.imageviewer
import androidx.compose.foundation.layout.displayCutoutPadding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
import example.imageviewer.model.PictureData
import kotlinx.coroutines.Dispatchers
import java.util.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Share
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.vector.ImageVector
import kotlinx.coroutines.Dispatchers
import java.util.UUID
actual fun Modifier.notchPadding(): Modifier = displayCutoutPadding().statusBarsPadding()

View File

@@ -1,7 +1,6 @@
package example.imageviewer.view
import android.annotation.SuppressLint
import android.location.Location
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCapture.OnImageCapturedCallback
@@ -11,10 +10,9 @@ import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Button
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -28,8 +26,8 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import com.google.android.gms.location.CurrentLocationRequest
import com.google.android.gms.location.LocationServices
import com.google.android.gms.tasks.Task
import example.imageviewer.*
import example.imageviewer.icon.IconPhotoCamera
import example.imageviewer.model.GpsPosition
import example.imageviewer.model.PictureData
import example.imageviewer.model.createCameraPictureData
@@ -104,57 +102,57 @@ private fun CameraWithGrantedPermission(
}
val nameAndDescription = createNewPhotoNameAndDescription()
var capturePhotoStarted by remember { mutableStateOf(false) }
Box(contentAlignment = Alignment.BottomCenter, modifier = modifier) {
Box(modifier = modifier) {
AndroidView({ previewView }, modifier = Modifier.fillMaxSize())
Button(
CircularButton(
imageVector = IconPhotoCamera,
modifier = Modifier.align(Alignment.BottomCenter).padding(36.dp),
enabled = !capturePhotoStarted,
onClick = {
fun addLocationInfoAndReturnResult(imageBitmap: ImageBitmap) {
fun sendToStorage(gpsPosition: GpsPosition) {
onCapture(
createCameraPictureData(
name = nameAndDescription.name,
description = nameAndDescription.description,
gps = gpsPosition
),
AndroidStorableImage(imageBitmap)
)
capturePhotoStarted = false
}
LocationServices.getFusedLocationProviderClient(context)
.getCurrentLocation(CurrentLocationRequest.Builder().build(), null)
.apply {
addOnSuccessListener {
sendToStorage(GpsPosition(it.latitude, it.longitude))
}
addOnFailureListener {
sendToStorage(GpsPosition(0.0, 0.0))
}
) {
fun addLocationInfoAndReturnResult(imageBitmap: ImageBitmap) {
fun sendToStorage(gpsPosition: GpsPosition) {
onCapture(
createCameraPictureData(
name = nameAndDescription.name,
description = nameAndDescription.description,
gps = gpsPosition
),
AndroidStorableImage(imageBitmap)
)
capturePhotoStarted = false
}
LocationServices.getFusedLocationProviderClient(context)
.getCurrentLocation(CurrentLocationRequest.Builder().build(), null)
.apply {
addOnSuccessListener {
sendToStorage(GpsPosition(it.latitude, it.longitude))
}
}
addOnFailureListener {
sendToStorage(GpsPosition(0.0, 0.0))
}
}
}
capturePhotoStarted = true
imageCapture.takePicture(executor, object : OnImageCapturedCallback() {
override fun onCaptureSuccess(image: ImageProxy) {
val byteArray: ByteArray = image.planes[0].buffer.toByteArray()
val imageBitmap = byteArray.toImageBitmap()
image.close()
addLocationInfoAndReturnResult(imageBitmap)
}
})
viewScope.launch {
// TODO: There is a known issue with Android emulator
// https://partnerissuetracker.corp.google.com/issues/161034252
// After 5 seconds delay, let's assume that the bug appears and publish a prepared photo
delay(5000)
if (capturePhotoStarted) {
addLocationInfoAndReturnResult(
resource("android-emulator-photo.jpg").readBytes().toImageBitmap()
)
}
capturePhotoStarted = true
imageCapture.takePicture(executor, object : OnImageCapturedCallback() {
override fun onCaptureSuccess(image: ImageProxy) {
val byteArray: ByteArray = image.planes[0].buffer.toByteArray()
val imageBitmap = byteArray.toImageBitmap()
image.close()
addLocationInfoAndReturnResult(imageBitmap)
}
}) {
Text(LocalLocalization.current.takePhoto, color = Color.White)
})
viewScope.launch {
// TODO: There is a known issue with Android emulator
// https://partnerissuetracker.corp.google.com/issues/161034252
// After 5 seconds delay, let's assume that the bug appears and publish a prepared photo
delay(5000)
if (capturePhotoStarted) {
addLocationInfoAndReturnResult(
resource("android-emulator-photo.jpg").readBytes().toImageBitmap()
)
}
}
}
if (capturePhotoStarted) {
CircularProgressIndicator(

View File

@@ -33,7 +33,7 @@ internal fun ImageViewerCommon(
internal fun ImageViewerWithProvidedDependencies(
pictures: SnapshotStateList<PictureData>
) {
val selectedPictureIndex: MutableState<Int> = mutableStateOf(0)
val selectedPictureIndex = remember { mutableStateOf(0) }
val navigationStack = remember { NavigationStack<Page>(GalleryPage()) }
val externalEvents = LocalInternalEvents.current
LaunchedEffect(Unit) {

View File

@@ -0,0 +1,24 @@
package example.imageviewer.icon
import androidx.compose.material.icons.materialIcon
import androidx.compose.material.icons.materialPath
val IconCustomArrowBack = materialIcon("Filled.CustomArrowBack") {
val startY = 12f
val startX = 1f
val arrowWidth = 8f
val arrowHeight = 14f
val lineWidth = 14f
val lineHeight = 2f
materialPath {
moveTo(startX, startY)
lineToRelative(arrowWidth, arrowHeight / 2)
verticalLineToRelative(-arrowHeight)
close()
moveTo(startX + arrowWidth, startY + lineHeight / 2)
verticalLineToRelative(-lineHeight)
horizontalLineToRelative(lineWidth)
verticalLineToRelative(lineHeight)
close()
}
}

View File

@@ -0,0 +1,53 @@
package example.imageviewer.icon
import androidx.compose.material.icons.materialIcon
import androidx.compose.material.icons.materialPath
// TODO Copied from "material:material-icons-extended", because this artifact is not working on iOS for now
val IconAutoFixHigh = materialIcon(name = "Filled.AutoFixHigh") {
materialPath {
moveTo(7.5f, 5.6f)
lineTo(10.0f, 7.0f)
lineTo(8.6f, 4.5f)
lineTo(10.0f, 2.0f)
lineTo(7.5f, 3.4f)
lineTo(5.0f, 2.0f)
lineToRelative(1.4f, 2.5f)
lineTo(5.0f, 7.0f)
close()
moveTo(19.5f, 15.4f)
lineTo(17.0f, 14.0f)
lineToRelative(1.4f, 2.5f)
lineTo(17.0f, 19.0f)
lineToRelative(2.5f, -1.4f)
lineTo(22.0f, 19.0f)
lineToRelative(-1.4f, -2.5f)
lineTo(22.0f, 14.0f)
close()
moveTo(22.0f, 2.0f)
lineToRelative(-2.5f, 1.4f)
lineTo(17.0f, 2.0f)
lineToRelative(1.4f, 2.5f)
lineTo(17.0f, 7.0f)
lineToRelative(2.5f, -1.4f)
lineTo(22.0f, 7.0f)
lineToRelative(-1.4f, -2.5f)
close()
moveTo(14.37f, 7.29f)
curveToRelative(-0.39f, -0.39f, -1.02f, -0.39f, -1.41f, 0.0f)
lineTo(1.29f, 18.96f)
curveToRelative(-0.39f, 0.39f, -0.39f, 1.02f, 0.0f, 1.41f)
lineToRelative(2.34f, 2.34f)
curveToRelative(0.39f, 0.39f, 1.02f, 0.39f, 1.41f, 0.0f)
lineTo(16.7f, 11.05f)
curveToRelative(0.39f, -0.39f, 0.39f, -1.02f, 0.0f, -1.41f)
lineToRelative(-2.33f, -2.35f)
close()
moveTo(13.34f, 12.78f)
lineToRelative(-2.12f, -2.12f)
lineToRelative(2.44f, -2.44f)
lineToRelative(2.12f, 2.12f)
lineToRelative(-2.44f, 2.44f)
close()
}
}

View File

@@ -0,0 +1,39 @@
package example.imageviewer.icon
import androidx.compose.material.icons.materialIcon
import androidx.compose.material.icons.materialPath
// TODO Copied from "material:material-icons-extended", because this artifact is not working on iOS for now
val IconIosShare = materialIcon(name = "Filled.IosShare") {
materialPath {
moveTo(16.0f, 5.0f)
lineToRelative(-1.42f, 1.42f)
lineToRelative(-1.59f, -1.59f)
lineTo(12.99f, 16.0f)
horizontalLineToRelative(-1.98f)
lineTo(11.01f, 4.83f)
lineTo(9.42f, 6.42f)
lineTo(8.0f, 5.0f)
lineToRelative(4.0f, -4.0f)
lineToRelative(4.0f, 4.0f)
close()
moveTo(20.0f, 10.0f)
verticalLineToRelative(11.0f)
curveToRelative(0.0f, 1.1f, -0.9f, 2.0f, -2.0f, 2.0f)
lineTo(6.0f, 23.0f)
curveToRelative(-1.11f, 0.0f, -2.0f, -0.9f, -2.0f, -2.0f)
lineTo(4.0f, 10.0f)
curveToRelative(0.0f, -1.11f, 0.89f, -2.0f, 2.0f, -2.0f)
horizontalLineToRelative(3.0f)
verticalLineToRelative(2.0f)
lineTo(6.0f, 10.0f)
verticalLineToRelative(11.0f)
horizontalLineToRelative(12.0f)
lineTo(18.0f, 10.0f)
horizontalLineToRelative(-3.0f)
lineTo(15.0f, 8.0f)
horizontalLineToRelative(3.0f)
curveToRelative(1.1f, 0.0f, 2.0f, 0.89f, 2.0f, 2.0f)
close()
}
}

View File

@@ -0,0 +1,32 @@
package example.imageviewer.icon
import androidx.compose.material.icons.materialIcon
import androidx.compose.material.icons.materialPath
// TODO Copied from "material:material-icons-extended", because this artifact is not working on iOS for now
val IconMap = materialIcon(name = "Filled.Map") {
materialPath {
moveTo(20.5f, 3.0f)
lineToRelative(-0.16f, 0.03f)
lineTo(15.0f, 5.1f)
lineTo(9.0f, 3.0f)
lineTo(3.36f, 4.9f)
curveToRelative(-0.21f, 0.07f, -0.36f, 0.25f, -0.36f, 0.48f)
verticalLineTo(20.5f)
curveToRelative(0.0f, 0.28f, 0.22f, 0.5f, 0.5f, 0.5f)
lineToRelative(0.16f, -0.03f)
lineTo(9.0f, 18.9f)
lineToRelative(6.0f, 2.1f)
lineToRelative(5.64f, -1.9f)
curveToRelative(0.21f, -0.07f, 0.36f, -0.25f, 0.36f, -0.48f)
verticalLineTo(3.5f)
curveToRelative(0.0f, -0.28f, -0.22f, -0.5f, -0.5f, -0.5f)
close()
moveTo(15.0f, 19.0f)
lineToRelative(-6.0f, -2.11f)
verticalLineTo(5.0f)
lineToRelative(6.0f, 2.11f)
verticalLineTo(19.0f)
close()
}
}

View File

@@ -0,0 +1,28 @@
package example.imageviewer.icon
import androidx.compose.material.icons.materialIcon
import androidx.compose.material.icons.materialPath
// TODO Copied from "material:material-icons-extended", because this artifact is not working on iOS for now
val IconMenu = materialIcon(name = "Filled.Menu") {
materialPath {
moveTo(3.0f, 18.0f)
horizontalLineToRelative(18.0f)
verticalLineToRelative(-2.0f)
lineTo(3.0f, 16.0f)
verticalLineToRelative(2.0f)
close()
moveTo(3.0f, 13.0f)
horizontalLineToRelative(18.0f)
verticalLineToRelative(-2.0f)
lineTo(3.0f, 11.0f)
verticalLineToRelative(2.0f)
close()
moveTo(3.0f, 6.0f)
verticalLineToRelative(2.0f)
horizontalLineToRelative(18.0f)
lineTo(21.0f, 6.0f)
lineTo(3.0f, 6.0f)
close()
}
}

View File

@@ -0,0 +1,28 @@
package example.imageviewer.icon
import androidx.compose.material.icons.materialIcon
import androidx.compose.material.icons.materialPath
// TODO Copied from "material:material-icons-extended", because this artifact is not working on iOS for now
val IconMoreVert = materialIcon(name = "Filled.MoreVert") {
materialPath {
moveTo(12.0f, 8.0f)
curveToRelative(1.1f, 0.0f, 2.0f, -0.9f, 2.0f, -2.0f)
reflectiveCurveToRelative(-0.9f, -2.0f, -2.0f, -2.0f)
reflectiveCurveToRelative(-2.0f, 0.9f, -2.0f, 2.0f)
reflectiveCurveToRelative(0.9f, 2.0f, 2.0f, 2.0f)
close()
moveTo(12.0f, 10.0f)
curveToRelative(-1.1f, 0.0f, -2.0f, 0.9f, -2.0f, 2.0f)
reflectiveCurveToRelative(0.9f, 2.0f, 2.0f, 2.0f)
reflectiveCurveToRelative(2.0f, -0.9f, 2.0f, -2.0f)
reflectiveCurveToRelative(-0.9f, -2.0f, -2.0f, -2.0f)
close()
moveTo(12.0f, 16.0f)
curveToRelative(-1.1f, 0.0f, -2.0f, 0.9f, -2.0f, 2.0f)
reflectiveCurveToRelative(0.9f, 2.0f, 2.0f, 2.0f)
reflectiveCurveToRelative(2.0f, -0.9f, 2.0f, -2.0f)
reflectiveCurveToRelative(-0.9f, -2.0f, -2.0f, -2.0f)
close()
}
}

View File

@@ -0,0 +1,36 @@
package example.imageviewer.icon
import androidx.compose.material.icons.materialIcon
import androidx.compose.material.icons.materialPath
// TODO Copied from "material:material-icons-extended", because this artifact is not working on iOS for now
val IconPhotoCamera = materialIcon(name = "Filled.PhotoCamera") {
materialPath {
moveTo(12.0f, 12.0f)
moveToRelative(-3.2f, 0.0f)
arcToRelative(3.2f, 3.2f, 0.0f, true, true, 6.4f, 0.0f)
arcToRelative(3.2f, 3.2f, 0.0f, true, true, -6.4f, 0.0f)
}
materialPath {
moveTo(9.0f, 2.0f)
lineTo(7.17f, 4.0f)
lineTo(4.0f, 4.0f)
curveToRelative(-1.1f, 0.0f, -2.0f, 0.9f, -2.0f, 2.0f)
verticalLineToRelative(12.0f)
curveToRelative(0.0f, 1.1f, 0.9f, 2.0f, 2.0f, 2.0f)
horizontalLineToRelative(16.0f)
curveToRelative(1.1f, 0.0f, 2.0f, -0.9f, 2.0f, -2.0f)
lineTo(22.0f, 6.0f)
curveToRelative(0.0f, -1.1f, -0.9f, -2.0f, -2.0f, -2.0f)
horizontalLineToRelative(-3.17f)
lineTo(15.0f, 2.0f)
lineTo(9.0f, 2.0f)
close()
moveTo(12.0f, 17.0f)
curveToRelative(-2.76f, 0.0f, -5.0f, -2.24f, -5.0f, -5.0f)
reflectiveCurveToRelative(2.24f, -5.0f, 5.0f, -5.0f)
reflectiveCurveToRelative(5.0f, 2.24f, 5.0f, 5.0f)
reflectiveCurveToRelative(-2.24f, 5.0f, -5.0f, 5.0f)
close()
}
}

View File

@@ -0,0 +1,28 @@
package example.imageviewer.icon
import androidx.compose.material.icons.materialIcon
import androidx.compose.material.icons.materialPath
// TODO Copied from "material:material-icons-extended", because this artifact is not working on iOS for now
val IconVisibility = materialIcon(name = "Filled.Visibility") {
materialPath {
moveTo(12.0f, 4.5f)
curveTo(7.0f, 4.5f, 2.73f, 7.61f, 1.0f, 12.0f)
curveToRelative(1.73f, 4.39f, 6.0f, 7.5f, 11.0f, 7.5f)
reflectiveCurveToRelative(9.27f, -3.11f, 11.0f, -7.5f)
curveToRelative(-1.73f, -4.39f, -6.0f, -7.5f, -11.0f, -7.5f)
close()
moveTo(12.0f, 17.0f)
curveToRelative(-2.76f, 0.0f, -5.0f, -2.24f, -5.0f, -5.0f)
reflectiveCurveToRelative(2.24f, -5.0f, 5.0f, -5.0f)
reflectiveCurveToRelative(5.0f, 2.24f, 5.0f, 5.0f)
reflectiveCurveToRelative(-2.24f, 5.0f, -5.0f, 5.0f)
close()
moveTo(12.0f, 9.0f)
curveToRelative(-1.66f, 0.0f, -3.0f, 1.34f, -3.0f, 3.0f)
reflectiveCurveToRelative(1.34f, 3.0f, 3.0f, 3.0f)
reflectiveCurveToRelative(3.0f, -1.34f, 3.0f, -3.0f)
reflectiveCurveToRelative(-1.34f, -3.0f, -3.0f, -3.0f)
close()
}
}

View File

@@ -1,46 +1,67 @@
package example.imageviewer.view
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import example.imageviewer.LocalLocalization
import example.imageviewer.icon.IconCustomArrowBack
import example.imageviewer.style.ImageviewerColors
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.painterResource
@Composable
internal fun CircularButton(
image: Painter,
content: @Composable () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean,
onClick: () -> Unit,
) {
Box(
modifier.size(54.dp).clip(CircleShape).background(ImageviewerColors.uiLightBlack)
.clickable { onClick() }, contentAlignment = Alignment.Center
modifier
.size(60.dp)
.clip(CircleShape)
.background(ImageviewerColors.uiLightBlack)
.run {
if (enabled) {
clickable { onClick() }
} else this
},
contentAlignment = Alignment.Center,
) {
Image(
image,
contentDescription = null,
modifier = Modifier.size(20.dp)
)
content()
}
}
@OptIn(ExperimentalResourceApi::class)
@Composable
internal fun CircularButton(
imageVector: ImageVector,
modifier: Modifier = Modifier,
enabled: Boolean = true,
onClick: () -> Unit,
) {
CircularButton(
modifier = modifier,
content = {
Icon(imageVector, null, Modifier.size(34.dp), Color.White)
},
enabled = enabled,
onClick = onClick
)
}
@Composable
internal fun BackButton(onClick: () -> Unit) {
Tooltip(LocalLocalization.current.back) {
CircularButton(
painterResource("arrowleft.png"),
imageVector = IconCustomArrowBack,
onClick = onClick
)
}

View File

@@ -1,7 +1,12 @@
@file:OptIn(ExperimentalResourceApi::class)
package example.imageviewer.view
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.AnimationConstants
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -9,26 +14,38 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.runtime.*
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import example.imageviewer.*
import example.imageviewer.icon.IconMenu
import example.imageviewer.icon.IconVisibility
import example.imageviewer.model.*
import example.imageviewer.style.ImageviewerColors
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.painterResource
import kotlinx.coroutines.launch
import kotlin.math.absoluteValue
enum class GalleryStyle {
SQUARES,
LIST
}
@OptIn(ExperimentalResourceApi::class)
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun GalleryScreen(
pictures: SnapshotStateList<PictureData>,
@@ -36,22 +53,45 @@ internal fun GalleryScreen(
onClickPreviewPicture: (PictureData) -> Unit,
onMakeNewMemory: () -> Unit
) {
val imageProvider = LocalImageProvider.current
val viewScope = rememberCoroutineScope()
val pagerState = rememberPagerState(initialPage = selectedPictureIndex.value)
LaunchedEffect(pagerState) {
// Subscribe to page changes
snapshotFlow { pagerState.currentPage }.collect { page ->
selectedPictureIndex.value = page
}
}
fun nextImage() {
selectedPictureIndex.value =
(selectedPictureIndex.value + 1).mod(pictures.size)
viewScope.launch {
pagerState.animateScrollToPage(
(pagerState.currentPage + 1).mod(pictures.size)
)
}
}
fun previousImage() {
selectedPictureIndex.value =
(selectedPictureIndex.value - 1).mod(pictures.size)
viewScope.launch {
pagerState.animateScrollToPage(
(pagerState.currentPage - 1).mod(pictures.size)
)
}
}
fun selectPicture(picture: PictureData) {
selectedPictureIndex.value = pictures.indexOfFirst { it == picture }
fun selectPicture(index: Int) {
viewScope.launch {
pagerState.animateScrollToPage(
index,
animationSpec = tween(
easing = LinearOutSlowInEasing,
durationMillis = AnimationConstants.DefaultDurationMillis * 2
)
)
}
}
val picture = pictures.getOrNull(selectedPictureIndex.value)
var galleryStyle by remember { mutableStateOf(GalleryStyle.SQUARES) }
val externalEvents = LocalInternalEvents.current
LaunchedEffect(Unit) {
@@ -63,20 +103,43 @@ internal fun GalleryScreen(
}
}
}
Column(modifier = Modifier.background(MaterialTheme.colors.background)) {
Box {
picture?.let {
PreviewImage(
picture = it, onClick = {
onClickPreviewPicture(it)
Box(
Modifier.fillMaxWidth().height(393.dp)
.background(Color.Black),
contentAlignment = Alignment.Center
) {
Box(
Modifier.fillMaxSize()
.clickable {
onClickPreviewPicture(pictures[pagerState.currentPage])
}
) {
HorizontalPager(pictures.size, state = pagerState) { idx ->
val picture = pictures[idx]
var image: ImageBitmap? by remember(picture) { mutableStateOf(null) }
LaunchedEffect(picture) {
image = imageProvider.getImage(picture)
}
if (image != null) {
Box(Modifier.fillMaxSize().animatePageChanges(pagerState, idx)) {
Image(
bitmap = image!!,
contentDescription = null,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
MemoryTextOverlay(picture)
}
}
}
)
}
}
TopLayout(
alignLeftContent = {},
alignRightContent = {
CircularButton(painterResource("list_view.png")) {
CircularButton(imageVector = IconMenu) {
galleryStyle = when (galleryStyle) {
GalleryStyle.SQUARES -> GalleryStyle.LIST
GalleryStyle.LIST -> GalleryStyle.SQUARES
@@ -89,7 +152,7 @@ internal fun GalleryScreen(
when (galleryStyle) {
GalleryStyle.SQUARES -> SquaresGalleryView(
images = pictures,
selectedImage = picture,
pagerState = pagerState,
onSelect = { selectPicture(it) },
)
@@ -100,7 +163,7 @@ internal fun GalleryScreen(
)
}
CircularButton(
image = painterResource("plus.png"),
Icons.Filled.Add,
modifier = Modifier.align(Alignment.BottomCenter).padding(36.dp),
onClick = onMakeNewMemory,
)
@@ -108,11 +171,12 @@ internal fun GalleryScreen(
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun SquaresGalleryView(
images: List<PictureData>,
selectedImage: PictureData?,
onSelect: (PictureData) -> Unit,
pagerState: PagerState,
onSelect: (Int) -> Unit,
) {
LazyVerticalGrid(
modifier = Modifier.padding(top = 4.dp),
@@ -121,17 +185,15 @@ private fun SquaresGalleryView(
horizontalArrangement = Arrangement.spacedBy(1.dp)
) {
itemsIndexed(images) { idx, picture ->
val isSelected = picture == selectedImage
SquareThumbnail(
picture = picture,
onClick = { onSelect(picture) },
isHighlighted = isSelected
onClick = { onSelect(idx) },
isHighlighted = pagerState.targetPage == idx
)
}
}
}
@OptIn(ExperimentalResourceApi::class)
@Composable
internal fun SquareThumbnail(
picture: PictureData,
@@ -139,8 +201,7 @@ internal fun SquareThumbnail(
onClick: () -> Unit
) {
Box(
Modifier.aspectRatio(1.0f).clickable(onClick = onClick),
contentAlignment = Alignment.BottomEnd
Modifier.aspectRatio(1.0f).clickable(onClick = onClick)
) {
Tooltip(picture.name) {
ThumbnailImage(
@@ -148,27 +209,33 @@ internal fun SquareThumbnail(
picture = picture,
)
}
if (isHighlighted) {
Box(Modifier.fillMaxSize().background(ImageviewerColors.uiLightBlack))
Box(
Modifier
.padding(end = 4.dp, bottom = 4.dp)
.clip(CircleShape)
.width(32.dp)
.background(ImageviewerColors.uiLightBlack)
.aspectRatio(1.0f)
.clickable {
onClick()
},
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource("eye.png"),
contentDescription = null,
modifier = Modifier
.width(17.dp)
.height(17.dp),
)
val tween = tween<Float>(
durationMillis = AnimationConstants.DefaultDurationMillis * 3,
delayMillis = 100,
easing = LinearOutSlowInEasing,
)
AnimatedVisibility(isHighlighted, enter = fadeIn(tween), exit = fadeOut(tween)) {
Box(Modifier.fillMaxSize().background(ImageviewerColors.uiLightBlack)) {
Box(
Modifier
.align(Alignment.BottomEnd)
.padding(end = 4.dp, bottom = 4.dp)
.clip(CircleShape)
.width(32.dp)
.background(ImageviewerColors.uiLightBlack)
.aspectRatio(1.0f)
.clickable {
onClick()
},
contentAlignment = Alignment.Center
) {
Icon(
imageVector = IconVisibility,
contentDescription = null,
modifier = Modifier.size(17.dp),
tint = Color.White,
)
}
}
}
}
@@ -177,7 +244,7 @@ internal fun SquareThumbnail(
@Composable
private fun ListGalleryView(
pictures: List<PictureData>,
onSelect: (PictureData) -> Unit,
onSelect: (Int) -> Unit,
onFullScreen: (PictureData) -> Unit,
) {
val notification = LocalNotification.current
@@ -189,7 +256,7 @@ private fun ListGalleryView(
Thumbnail(
picture = p.value,
onClickSelect = {
onSelect(p.value)
onSelect(p.index)
},
onClickFullScreen = {
onFullScreen(p.value)
@@ -202,3 +269,14 @@ private fun ListGalleryView(
}
}
}
@OptIn(ExperimentalFoundationApi::class)
private fun Modifier.animatePageChanges(pagerState: PagerState, index: Int) =
graphicsLayer {
val x = (pagerState.currentPage - index + pagerState.currentPageOffsetFraction) * 2
alpha = 1f - (x.absoluteValue * 0.7f).coerceIn(0f, 0.7f)
val scale = 1f - (x.absoluteValue * 0.4f).coerceIn(0f, 0.4f)
scaleX = scale
scaleY = scale
rotationY = x * 15f
}

View File

@@ -32,6 +32,7 @@ import androidx.compose.ui.unit.sp
import example.imageviewer.LocalImageProvider
import example.imageviewer.LocalSharePicture
import example.imageviewer.filter.getPlatformContext
import example.imageviewer.icon.IconAutoFixHigh
import example.imageviewer.isShareFeatureSupported
import example.imageviewer.model.*
import example.imageviewer.shareIcon
@@ -182,9 +183,12 @@ private fun MemoryHeader(bitmap: ImageBitmap, picture: PictureData, onClick: ()
@Composable
internal fun BoxScope.MagicButtonOverlay(onClick: () -> Unit) {
Column(
modifier = Modifier.align(Alignment.BottomEnd).padding(end = 12.dp, bottom = 16.dp)
modifier = Modifier.align(Alignment.BottomEnd).padding(12.dp)
) {
CircularButton(painterResource("magic.png"), onClick = onClick)
CircularButton(
imageVector = IconAutoFixHigh,
onClick = onClick,
)
}
}

View File

@@ -1,85 +0,0 @@
package example.imageviewer.view
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedContentScope
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.animation.with
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import example.imageviewer.LocalImageProvider
import example.imageviewer.model.PictureData
@OptIn(ExperimentalAnimationApi::class)
@Composable
internal fun PreviewImage(
picture: PictureData,
onClick: () -> Unit,
) {
val imageProvider = LocalImageProvider.current
val interactionSource = remember { MutableInteractionSource() }
Box(
Modifier.fillMaxWidth().height(393.dp).background(Color.Black),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.fillMaxSize()
.clickable(interactionSource, indication = null, onClick = onClick),
) {
AnimatedContent(
targetState = picture,
transitionSpec = {
slideIntoContainer(
towards = AnimatedContentScope.SlideDirection.Left,
animationSpec = tween(durationMillis = 500, easing = FastOutSlowInEasing)
) with slideOutOfContainer(
towards = AnimatedContentScope.SlideDirection.Left,
animationSpec = tween(durationMillis = 500, easing = FastOutSlowInEasing)
)
}
) { currentPicture ->
var image: ImageBitmap? by remember(currentPicture) { mutableStateOf(null) }
LaunchedEffect(currentPicture) {
image = imageProvider.getImage(currentPicture)
}
if (image != null) {
Box(Modifier.fillMaxSize()) {
Image(
bitmap = image!!,
contentDescription = null,
modifier = Modifier
.fillMaxSize(),
contentScale = ContentScale.Crop
)
MemoryTextOverlay(currentPicture)
}
} else {
Spacer(
modifier = Modifier.fillMaxSize()
)
}
}
}
}
}

View File

@@ -1,25 +1,30 @@
package example.imageviewer.view
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import example.imageviewer.icon.IconMoreVert
import example.imageviewer.model.PictureData
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.painterResource
@OptIn(ExperimentalResourceApi::class)
@Composable
internal fun Thumbnail(
picture: PictureData,
@@ -53,14 +58,14 @@ internal fun Thumbnail(
style = MaterialTheme.typography.subtitle1
)
Image(
painterResource("dots.png"),
contentDescription = null,
Icon(
imageVector = IconMoreVert,
contentDescription = "more info",
modifier = Modifier.height(70.dp)
.width(30.dp)
.padding(start = 1.dp, top = 25.dp, end = 1.dp, bottom = 25.dp)
.clickable { onClickInfo() },
contentScale = ContentScale.FillHeight
tint = Color.DarkGray
)
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

View File

@@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import example.imageviewer.*
import example.imageviewer.icon.IconPhotoCamera
import example.imageviewer.model.PictureData
import example.imageviewer.model.createCameraPictureData
import org.jetbrains.compose.resources.ExperimentalResourceApi
@@ -51,7 +52,10 @@ internal actual fun CameraView(
.padding(20.dp)
)
val nameAndDescription = createNewPhotoNameAndDescription()
Button(onClick = {
CircularButton(
imageVector = IconPhotoCamera,
modifier = Modifier.align(Alignment.BottomCenter).padding(36.dp),
) {
onCapture(
createCameraPictureData(
name = nameAndDescription.name,
@@ -60,8 +64,6 @@ internal actual fun CameraView(
),
DesktopStorableImage(imageBitmap)
)
}, Modifier.align(Alignment.BottomCenter)) {
Text(LocalLocalization.current.takePhoto)
}
}
}

View File

@@ -29,8 +29,9 @@ import java.awt.Toolkit
class ExternalNavigationEventBus {
private val _events = MutableSharedFlow<ExternalImageViewerEvent>(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_LATEST
replay = 0,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
extraBufferCapacity = 1,
)
val events = _events.asSharedFlow()

View File

@@ -1,41 +0,0 @@
package example.imageviewer
import androidx.compose.material.icons.materialIcon
import androidx.compose.material.icons.materialPath
import androidx.compose.ui.graphics.vector.ImageVector
// TODO Copied from material3, because "material:material-icons-extended" not working on iOS for now
val IosShareIcon: ImageVector =
materialIcon(name = "Filled.IosShare") {
materialPath {
moveTo(16.0f, 5.0f)
lineToRelative(-1.42f, 1.42f)
lineToRelative(-1.59f, -1.59f)
lineTo(12.99f, 16.0f)
horizontalLineToRelative(-1.98f)
lineTo(11.01f, 4.83f)
lineTo(9.42f, 6.42f)
lineTo(8.0f, 5.0f)
lineToRelative(4.0f, -4.0f)
lineToRelative(4.0f, 4.0f)
close()
moveTo(20.0f, 10.0f)
verticalLineToRelative(11.0f)
curveToRelative(0.0f, 1.1f, -0.9f, 2.0f, -2.0f, 2.0f)
lineTo(6.0f, 23.0f)
curveToRelative(-1.11f, 0.0f, -2.0f, -0.9f, -2.0f, -2.0f)
lineTo(4.0f, 10.0f)
curveToRelative(0.0f, -1.11f, 0.89f, -2.0f, 2.0f, -2.0f)
horizontalLineToRelative(3.0f)
verticalLineToRelative(2.0f)
lineTo(6.0f, 10.0f)
verticalLineToRelative(11.0f)
horizontalLineToRelative(12.0f)
lineTo(18.0f, 10.0f)
horizontalLineToRelative(-3.0f)
lineTo(15.0f, 8.0f)
horizontalLineToRelative(3.0f)
curveToRelative(1.1f, 0.0f, 2.0f, 0.89f, 2.0f, 2.0f)
close()
}
}

View File

@@ -6,6 +6,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import example.imageviewer.icon.IconIosShare
import kotlinx.cinterop.useContents
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
@@ -48,4 +49,4 @@ actual val ioDispatcher = Dispatchers.IO
actual val isShareFeatureSupported: Boolean = true
actual val shareIcon: ImageVector = IosShareIcon
actual val shareIcon: ImageVector = IconIosShare

View File

@@ -2,7 +2,6 @@ package example.imageviewer.view
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Text
import androidx.compose.runtime.*
@@ -12,9 +11,9 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.interop.UIKitView
import androidx.compose.ui.unit.dp
import example.imageviewer.IosStorableImage
import example.imageviewer.LocalLocalization
import example.imageviewer.PlatformStorableImage
import example.imageviewer.createNewPhotoNameAndDescription
import example.imageviewer.icon.IconPhotoCamera
import example.imageviewer.model.GpsPosition
import example.imageviewer.model.PictureData
import example.imageviewer.model.createCameraPictureData
@@ -244,27 +243,25 @@ private fun BoxScope.RealDeviceCamera(
CATransaction.commit()
},
)
Button(
modifier = Modifier.align(Alignment.BottomCenter).padding(44.dp),
CircularButton(
imageVector = IconPhotoCamera,
modifier = Modifier.align(Alignment.BottomCenter).padding(36.dp),
enabled = !capturePhotoStarted,
onClick = {
capturePhotoStarted = true
val photoSettings = AVCapturePhotoSettings.photoSettingsWithFormat(
format = mapOf(AVVideoCodecKey to AVVideoCodecTypeJPEG)
)
if (camera.position == AVCaptureDevicePositionFront) {
capturePhotoOutput.connectionWithMediaType(AVMediaTypeVideo)
?.automaticallyAdjustsVideoMirroring = false
capturePhotoOutput.connectionWithMediaType(AVMediaTypeVideo)
?.videoMirrored = true
}
capturePhotoOutput.capturePhotoWithSettings(
settings = photoSettings,
delegate = photoCaptureDelegate
)
}
) {
Text(LocalLocalization.current.takePhoto)
capturePhotoStarted = true
val photoSettings = AVCapturePhotoSettings.photoSettingsWithFormat(
format = mapOf(AVVideoCodecKey to AVVideoCodecTypeJPEG)
)
if (camera.position == AVCaptureDevicePositionFront) {
capturePhotoOutput.connectionWithMediaType(AVMediaTypeVideo)
?.automaticallyAdjustsVideoMirroring = false
capturePhotoOutput.connectionWithMediaType(AVMediaTypeVideo)
?.videoMirrored = true
}
capturePhotoOutput.capturePhotoWithSettings(
settings = photoSettings,
delegate = photoCaptureDelegate
)
}
if (capturePhotoStarted) {
CircularProgressIndicator(