mirror of
https://github.com/jlengrand/compose-multiplatform.git
synced 2026-03-10 08:11:20 +00:00
First steps in implementing redesigned UI (#2817)
Co-authored-by: Sebastian.Aigner <Sebastian.Aigner@de-unit-1151.fritz.box>
This commit is contained in:
@@ -19,6 +19,16 @@ object ImageviewerColors {
|
||||
val TranslucentWhite = Color(255, 255, 255, 20)
|
||||
val Transparent = Color.Transparent
|
||||
|
||||
val background = Color(0xFFFFFFFF)
|
||||
val onBackground = Color(0xFF19191C)
|
||||
|
||||
val fullScreenImageBackground = Color(0xFF19191C)
|
||||
|
||||
val uiLightBlack = Color(25, 25, 28, 128)
|
||||
val textOnImage = Color.White
|
||||
val noteBlockBackground = Color(0xFFF3F3F4)
|
||||
|
||||
|
||||
val KotlinGradient0 = Color(0xFF7F52FF)
|
||||
val KotlinGradient50 = Color(0xFFC811E2)
|
||||
val KotlinGradient100 = Color(0xFFE54857)
|
||||
@@ -39,8 +49,8 @@ internal fun ImageViewerTheme(content: @Composable () -> Unit) {
|
||||
isSystemInDarkTheme() // todo check and change colors
|
||||
MaterialTheme(
|
||||
colorScheme = MaterialTheme.colorScheme.copy(
|
||||
background = Color(0xFF1B1B1B),
|
||||
onBackground = Color(0xFFFFFFFF)
|
||||
background = ImageviewerColors.background,
|
||||
onBackground = ImageviewerColors.onBackground
|
||||
)
|
||||
) {
|
||||
content()
|
||||
|
||||
@@ -5,8 +5,10 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
@@ -57,7 +59,7 @@ internal fun FullscreenImage(
|
||||
null
|
||||
}
|
||||
}
|
||||
Box(Modifier.fillMaxSize().background(color = MaterialTheme.colorScheme.background)) {
|
||||
Box(Modifier.fillMaxSize().background(color = ImageviewerColors.fullScreenImageBackground)) {
|
||||
Column {
|
||||
FullscreenImageBar(
|
||||
localization,
|
||||
@@ -73,31 +75,52 @@ internal fun FullscreenImage(
|
||||
}
|
||||
})
|
||||
if (imageWithFilter != null) {
|
||||
val imageSize = IntSize(imageWithFilter.width, imageWithFilter.height)
|
||||
val scalableState = remember(imageSize) { ScalableState(imageSize) }
|
||||
val visiblePartOfImage: IntRect = scalableState.visiblePart
|
||||
Slider(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = scalableState.scale,
|
||||
valueRange = MIN_SCALE..MAX_SCALE,
|
||||
onValueChange = { scalableState.setScale(it) },
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
.onGloballyPositioned { coordinates ->
|
||||
scalableState.changeBoxSize(coordinates.size)
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.BottomCenter) {
|
||||
val imageSize = IntSize(imageWithFilter.width, imageWithFilter.height)
|
||||
val scalableState = remember(imageSize) { ScalableState(imageSize) }
|
||||
val visiblePartOfImage: IntRect = scalableState.visiblePart
|
||||
Column {
|
||||
Slider(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = scalableState.scale,
|
||||
valueRange = MIN_SCALE..MAX_SCALE,
|
||||
onValueChange = { scalableState.setScale(it) },
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
.onGloballyPositioned { coordinates ->
|
||||
scalableState.changeBoxSize(coordinates.size)
|
||||
}
|
||||
.addUserInput(scalableState)
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
painter = BitmapPainter(
|
||||
imageWithFilter,
|
||||
srcOffset = visiblePartOfImage.topLeft,
|
||||
srcSize = visiblePartOfImage.size
|
||||
),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
.addUserInput(scalableState)
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
painter = BitmapPainter(
|
||||
imageWithFilter,
|
||||
srcOffset = visiblePartOfImage.topLeft,
|
||||
srcSize = visiblePartOfImage.size
|
||||
),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
Box(
|
||||
Modifier.clip(RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp))
|
||||
.background(ImageviewerColors.fullScreenImageBackground).padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
) {
|
||||
FilterButtons(availableFilters, selectedFilters, {
|
||||
if (it !in selectedFilters) {
|
||||
selectedFilters += it
|
||||
} else {
|
||||
selectedFilters -= it
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LoadingScreen()
|
||||
@@ -118,7 +141,7 @@ private fun FullscreenImageBar(
|
||||
onSelectFilter: (FilterType) -> Unit
|
||||
) {
|
||||
TopAppBar(
|
||||
modifier = Modifier.background(brush = ImageviewerColors.kotlinHorizontalGradientBrush),
|
||||
modifier = Modifier.background(color = ImageviewerColors.fullScreenImageBackground),
|
||||
colors = TopAppBarDefaults.smallTopAppBarColors(
|
||||
containerColor = ImageviewerColors.Transparent,
|
||||
titleContentColor = MaterialTheme.colorScheme.onBackground
|
||||
@@ -128,27 +151,30 @@ private fun FullscreenImageBar(
|
||||
},
|
||||
navigationIcon = {
|
||||
Tooltip(localization.back) {
|
||||
Image(
|
||||
painterResource("back.png"),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(38.dp)
|
||||
.clip(CircleShape)
|
||||
.clickable { onBack() }
|
||||
CircularButton(
|
||||
painterResource("arrowleft.png"),
|
||||
onClick = { onBack() }
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
for (type in filters) {
|
||||
FilterButton(active = type in selectedFilters,
|
||||
type,
|
||||
onClick = {
|
||||
onSelectFilter(type)
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FilterButtons(
|
||||
filters: List<FilterType>,
|
||||
selectedFilters: Set<FilterType>,
|
||||
onSelectFilter: (FilterType) -> Unit
|
||||
) {
|
||||
for (type in filters) {
|
||||
FilterButton(active = type in selectedFilters,
|
||||
type,
|
||||
onClick = {
|
||||
onSelectFilter(type)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun FilterButton(
|
||||
@@ -165,14 +191,13 @@ private fun FilterButton(
|
||||
Image(
|
||||
getFilterImage(active, type = type),
|
||||
contentDescription = null,
|
||||
Modifier.size(38.dp)
|
||||
Modifier.size(40.dp)
|
||||
.hoverable(interactionSource)
|
||||
.background(color = ImageviewerColors.buttonBackground(filterButtonHover))
|
||||
.clickable { onClick() }
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.width(20.dp))
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalResourceApi::class)
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package example.imageviewer.view
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -15,15 +13,13 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
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.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.itemsIndexed
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -32,14 +28,10 @@ import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import example.imageviewer.Dependencies
|
||||
import example.imageviewer.ExternalImageViewerEvent
|
||||
import example.imageviewer.model.GalleryEntryWithMetadata
|
||||
@@ -48,24 +40,9 @@ import example.imageviewer.model.GalleryPage
|
||||
import example.imageviewer.model.PhotoGallery
|
||||
import example.imageviewer.model.bigUrl
|
||||
import example.imageviewer.style.ImageviewerColors
|
||||
import example.imageviewer.style.ImageviewerColors.kotlinHorizontalGradientBrush
|
||||
import org.jetbrains.compose.resources.ExperimentalResourceApi
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
|
||||
@Composable
|
||||
internal fun GalleryHeader() {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.padding(10.dp).fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
"My Gallery",
|
||||
fontSize = 25.sp,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
fontStyle = FontStyle.Italic
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
enum class GalleryStyle {
|
||||
SQUARES,
|
||||
@@ -91,32 +68,36 @@ internal fun GalleryScreen(
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
|
||||
TitleBar(
|
||||
onRefresh = { photoGallery.updatePictures() },
|
||||
onToggle = { galleryPage.toggleGalleryStyle() },
|
||||
dependencies
|
||||
)
|
||||
if (needShowPreview()) {
|
||||
PreviewImage(
|
||||
getImage = { dependencies.imageRepository.loadContent(it.bigUrl) },
|
||||
picture = galleryPage.picture, onClick = {
|
||||
galleryPage.pictureId?.let(onClickPreviewPicture)
|
||||
})
|
||||
Box {
|
||||
if (needShowPreview()) {
|
||||
PreviewImage(
|
||||
getImage = { dependencies.imageRepository.loadContent(it.bigUrl) },
|
||||
picture = galleryPage.picture, onClick = {
|
||||
galleryPage.pictureId?.let(onClickPreviewPicture)
|
||||
})
|
||||
}
|
||||
TitleBar(
|
||||
onRefresh = { photoGallery.updatePictures() },
|
||||
onToggle = { galleryPage.toggleGalleryStyle() },
|
||||
dependencies
|
||||
)
|
||||
}
|
||||
when (galleryPage.galleryStyle) {
|
||||
GalleryStyle.SQUARES -> SquaresGalleryView(
|
||||
pictures,
|
||||
galleryPage.pictureId,
|
||||
onSelect = { galleryPage.selectPicture(it) },
|
||||
onMakeNewMemory
|
||||
)
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.BottomCenter) {
|
||||
when (galleryPage.galleryStyle) {
|
||||
GalleryStyle.SQUARES -> SquaresGalleryView(
|
||||
pictures,
|
||||
galleryPage.pictureId,
|
||||
onSelect = { galleryPage.selectPicture(it) },
|
||||
)
|
||||
|
||||
GalleryStyle.LIST -> ListGalleryView(
|
||||
pictures,
|
||||
dependencies,
|
||||
onSelect = { galleryPage.selectPicture(it) },
|
||||
onFullScreen = { onClickPreviewPicture(it) }
|
||||
)
|
||||
GalleryStyle.LIST -> ListGalleryView(
|
||||
pictures,
|
||||
dependencies,
|
||||
onSelect = { galleryPage.selectPicture(it) },
|
||||
onFullScreen = { onClickPreviewPicture(it) }
|
||||
)
|
||||
}
|
||||
MakeNewMemoryMiniature(onMakeNewMemory)
|
||||
}
|
||||
}
|
||||
if (pictures.isEmpty()) {
|
||||
@@ -129,54 +110,95 @@ private fun SquaresGalleryView(
|
||||
images: List<GalleryEntryWithMetadata>,
|
||||
selectedImage: GalleryId?,
|
||||
onSelect: (GalleryId) -> Unit,
|
||||
onMakeNewMemory: () -> Unit,
|
||||
) {
|
||||
LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 128.dp)) {
|
||||
item {
|
||||
MakeNewMemoryMiniature(onMakeNewMemory)
|
||||
}
|
||||
itemsIndexed(images) { idx, image ->
|
||||
val isSelected = image.id == selectedImage
|
||||
val (picture, bitmap) = image
|
||||
SquareMiniature(
|
||||
image.thumbnail,
|
||||
onClick = { onSelect(picture) },
|
||||
isHighlighted = isSelected
|
||||
)
|
||||
Column {
|
||||
Spacer(Modifier.height(1.dp))
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Adaptive(minSize = 130.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(1.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(1.dp)
|
||||
) {
|
||||
itemsIndexed(images) { idx, image ->
|
||||
val isSelected = image.id == selectedImage
|
||||
val (picture, bitmap) = image
|
||||
SquareMiniature(
|
||||
image.thumbnail,
|
||||
onClick = { onSelect(picture) },
|
||||
isHighlighted = isSelected
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalResourceApi::class)
|
||||
@Composable
|
||||
private fun MakeNewMemoryMiniature(onClick: () -> Unit) {
|
||||
Box(
|
||||
Modifier.aspectRatio(1.0f)
|
||||
.clickable {
|
||||
onClick()
|
||||
}, contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
"+",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = 50.sp
|
||||
)
|
||||
Column {
|
||||
Box(
|
||||
Modifier
|
||||
.clip(CircleShape)
|
||||
.width(52.dp)
|
||||
.background(ImageviewerColors.uiLightBlack)
|
||||
.aspectRatio(1.0f)
|
||||
.clickable {
|
||||
onClick()
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource("plus.png"),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.width(18.dp)
|
||||
.height(18.dp),
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(32.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalResourceApi::class)
|
||||
@Composable
|
||||
internal fun SquareMiniature(image: ImageBitmap, isHighlighted: Boolean, onClick: () -> Unit) {
|
||||
Image(
|
||||
bitmap = image,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.aspectRatio(1.0f).clickable { onClick() }.then(
|
||||
if (isHighlighted) {
|
||||
Modifier.border(BorderStroke(5.dp, Color.White))
|
||||
} else Modifier
|
||||
),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
Box(
|
||||
Modifier.aspectRatio(1.0f).clickable { onClick() },
|
||||
contentAlignment = Alignment.BottomEnd
|
||||
) {
|
||||
Image(
|
||||
bitmap = image,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize().clickable { onClick() }.then(
|
||||
if (isHighlighted) {
|
||||
Modifier//.border(BorderStroke(5.dp, Color.White))
|
||||
} else Modifier
|
||||
),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -186,8 +208,6 @@ private fun ListGalleryView(
|
||||
onSelect: (GalleryId) -> Unit,
|
||||
onFullScreen: (GalleryId) -> Unit
|
||||
) {
|
||||
GalleryHeader()
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
ScrollableColumn(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
@@ -215,44 +235,14 @@ private fun ListGalleryView(
|
||||
@Composable
|
||||
private fun TitleBar(onRefresh: () -> Unit, onToggle: () -> Unit, dependencies: Dependencies) {
|
||||
TopAppBar(
|
||||
modifier = Modifier.background(brush = kotlinHorizontalGradientBrush),
|
||||
modifier = Modifier.padding(start = 12.dp, end = 12.dp),
|
||||
colors = TopAppBarDefaults.smallTopAppBarColors(
|
||||
containerColor = ImageviewerColors.Transparent,
|
||||
titleContentColor = MaterialTheme.colorScheme.onBackground
|
||||
),
|
||||
title = {
|
||||
Row(Modifier.height(50.dp)) {
|
||||
Text(
|
||||
dependencies.localization.appName,
|
||||
modifier = Modifier.weight(1f).align(Alignment.CenterVertically),
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Surface(
|
||||
color = ImageviewerColors.Transparent,
|
||||
modifier = Modifier.padding(end = 20.dp).align(Alignment.CenterVertically),
|
||||
shape = CircleShape
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource("list_view.png"),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(35.dp).clickable {
|
||||
onToggle()
|
||||
}
|
||||
)
|
||||
}
|
||||
Surface(
|
||||
color = ImageviewerColors.Transparent,
|
||||
modifier = Modifier.padding(end = 20.dp).align(Alignment.CenterVertically),
|
||||
shape = CircleShape
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource("refresh.png"),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(35.dp).clickable {
|
||||
onRefresh()
|
||||
}
|
||||
)
|
||||
}
|
||||
Row(Modifier.height(50.dp).fillMaxWidth(), horizontalArrangement = Arrangement.End) {
|
||||
CircularButton(painterResource("list_view.png")) { onToggle() }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package example.imageviewer.view
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
@@ -9,6 +11,7 @@ import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
@@ -22,9 +25,12 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.Shadow
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
@@ -49,38 +55,17 @@ internal fun MemoryScreen(
|
||||
) {
|
||||
val pictures by photoGallery.galleryStateFlow.collectAsState()
|
||||
val picture = pictures.first { it.id == memoryPage.galleryId }
|
||||
Column {
|
||||
TopAppBar(
|
||||
modifier = Modifier.background(brush = ImageviewerColors.kotlinHorizontalGradientBrush),
|
||||
colors = TopAppBarDefaults.smallTopAppBarColors(
|
||||
containerColor = ImageviewerColors.Transparent,
|
||||
titleContentColor = MaterialTheme.colorScheme.onBackground
|
||||
),
|
||||
title = {
|
||||
Text("")
|
||||
},
|
||||
navigationIcon = {
|
||||
Tooltip("Back") {
|
||||
Image(
|
||||
painterResource("back.png"),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(38.dp)
|
||||
.clip(CircleShape)
|
||||
.clickable { onBack() }
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
Box {
|
||||
val scrollState = memoryPage.scrollState
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(scrollState),
|
||||
.verticalScroll(scrollState)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(300.dp)
|
||||
.height(393.dp)
|
||||
.background(Color.White)
|
||||
.graphicsLayer {
|
||||
translationY = 0.5f * scrollState.value
|
||||
@@ -89,11 +74,11 @@ internal fun MemoryScreen(
|
||||
) {
|
||||
MemoryHeader(picture.thumbnail, onClick = { onHeaderClick(memoryPage.galleryId) })
|
||||
}
|
||||
Box(modifier = Modifier.background(ImageviewerColors.kotlinHorizontalGradientBrush)) {
|
||||
Box(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
|
||||
Column {
|
||||
Headliner("Where it happened")
|
||||
Headliner("Place")
|
||||
LocationVisualizer()
|
||||
Headliner("What happened")
|
||||
Headliner("Note")
|
||||
Collapsible(
|
||||
"""
|
||||
I took a picture with my iPhone 14 at 17:45. The picture ended up being 3024 x 4032 pixels. ✨
|
||||
@@ -106,22 +91,74 @@ internal fun MemoryScreen(
|
||||
Headliner("Related memories")
|
||||
RelatedMemoriesVisualizer(pictures, onSelectRelatedMemory)
|
||||
Spacer(Modifier.height(50.dp))
|
||||
Text(
|
||||
"Delete this memory",
|
||||
Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center,
|
||||
color = Color.White
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
8.dp,
|
||||
Alignment.CenterHorizontally
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Image(
|
||||
painterResource("trash.png"),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(14.dp)
|
||||
)
|
||||
Text(
|
||||
text = "Delete Memory",
|
||||
textAlign = TextAlign.Left,
|
||||
color = ImageviewerColors.onBackground,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Normal
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(50.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
TopAppBar(
|
||||
modifier = Modifier.padding(start = 12.dp, end = 12.dp),
|
||||
colors = TopAppBarDefaults.smallTopAppBarColors(
|
||||
containerColor = ImageviewerColors.Transparent,
|
||||
titleContentColor = MaterialTheme.colorScheme.onBackground
|
||||
),
|
||||
title = {
|
||||
Text("")
|
||||
},
|
||||
navigationIcon = {
|
||||
Tooltip("Back") {
|
||||
CircularButton(painterResource("arrowleft.png"), onClick = { onBack() })
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun CircularButton(image: Painter, onClick: () -> Unit) {
|
||||
Box(
|
||||
Modifier.size(40.dp).clip(CircleShape).background(ImageviewerColors.uiLightBlack)
|
||||
.clickable { onClick() }, contentAlignment = Alignment.Center
|
||||
) {
|
||||
Image(
|
||||
image,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MemoryHeader(bitmap: ImageBitmap, onClick: () -> Unit) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val shadowTextStyle = LocalTextStyle.current.copy(
|
||||
shadow = Shadow(
|
||||
color = Color.Black,
|
||||
offset = Offset(4f, 4f),
|
||||
blurRadius = 4f
|
||||
)
|
||||
)
|
||||
Box(modifier = Modifier.clickable(interactionSource, null, onClick = { onClick() })) {
|
||||
Image(
|
||||
bitmap,
|
||||
@@ -129,35 +166,27 @@ private fun MemoryHeader(bitmap: ImageBitmap, onClick: () -> Unit) {
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
Column(modifier = Modifier.align(Alignment.Center)) {
|
||||
Column(
|
||||
modifier = Modifier.align(Alignment.BottomStart).padding(start = 12.dp, bottom = 16.dp)
|
||||
) {
|
||||
Text(
|
||||
"Your Memory",
|
||||
textAlign = TextAlign.Center,
|
||||
textAlign = TextAlign.Left,
|
||||
color = Color.White,
|
||||
fontSize = 50.sp,
|
||||
fontSize = 20.sp,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
fontWeight = FontWeight.Black
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
style = shadowTextStyle
|
||||
)
|
||||
Spacer(Modifier.height(5.dp))
|
||||
|
||||
Text(
|
||||
"19th of April 2023",
|
||||
textAlign = TextAlign.Left,
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Normal,
|
||||
style = shadowTextStyle
|
||||
)
|
||||
Spacer(Modifier.height(30.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.border(
|
||||
width = 2.dp,
|
||||
color = Color.Black,
|
||||
shape = RoundedCornerShape(100.dp)
|
||||
)
|
||||
.clip(
|
||||
RoundedCornerShape(100.dp)
|
||||
)
|
||||
.background(Color.Black.copy(alpha = 0.7f)).padding(10.dp)
|
||||
) {
|
||||
Text(
|
||||
"19th of April 2023",
|
||||
textAlign = TextAlign.Center,
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -169,12 +198,18 @@ internal fun Collapsible(s: String) {
|
||||
val text = if (isCollapsed) s.lines().first() + "... (see more)" else s
|
||||
Text(
|
||||
text,
|
||||
fontSize = 12.sp,
|
||||
modifier = Modifier
|
||||
.padding(10.dp, 0.dp)
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.background(Color.White)
|
||||
.background(ImageviewerColors.noteBlockBackground)
|
||||
.padding(10.dp)
|
||||
.animateContentSize()
|
||||
.animateContentSize(
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioLowBouncy,
|
||||
stiffness = Spring.StiffnessLow
|
||||
)
|
||||
)
|
||||
.clickable(interactionSource = interctionSource, indication = null) {
|
||||
isCollapsed = !isCollapsed
|
||||
},
|
||||
@@ -185,10 +220,10 @@ internal fun Collapsible(s: String) {
|
||||
internal fun Headliner(s: String) {
|
||||
Text(
|
||||
text = s,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 32.sp,
|
||||
color = Color.White,
|
||||
modifier = Modifier.padding(10.dp, 30.dp, 10.dp, 10.dp)
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 20.sp,
|
||||
color = Color.Black,
|
||||
modifier = Modifier.padding(start = 12.dp, top = 32.dp, end = 12.dp, bottom = 16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -212,7 +247,10 @@ internal fun RelatedMemoriesVisualizer(
|
||||
modifier = Modifier.padding(10.dp, 0.dp).clip(RoundedCornerShape(10.dp)).fillMaxWidth()
|
||||
.height(200.dp)
|
||||
) {
|
||||
LazyRow(modifier = Modifier.fillMaxSize()) {
|
||||
LazyRow(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
itemsIndexed(ps) { idx, item ->
|
||||
RelatedMemory(idx, item, onSelectRelatedMemory)
|
||||
}
|
||||
@@ -226,8 +264,10 @@ internal fun RelatedMemory(
|
||||
galleryEntry: GalleryEntryWithMetadata,
|
||||
onSelectRelatedMemory: (GalleryId) -> Unit
|
||||
) {
|
||||
SquareMiniature(
|
||||
galleryEntry.thumbnail,
|
||||
false,
|
||||
onClick = { onSelectRelatedMemory(galleryEntry.id) })
|
||||
Box(Modifier.size(130.dp).clip(RoundedCornerShape(8.dp))) {
|
||||
SquareMiniature(
|
||||
galleryEntry.thumbnail,
|
||||
false,
|
||||
onClick = { onSelectRelatedMemory(galleryEntry.id) })
|
||||
}
|
||||
}
|
||||
@@ -2,31 +2,30 @@ package example.imageviewer.view
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.animation.with
|
||||
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.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Card
|
||||
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.ImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import example.imageviewer.model.Picture
|
||||
import example.imageviewer.style.ImageviewerColors.kotlinHorizontalGradientBrush
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
@@ -41,41 +40,47 @@ internal fun PreviewImage(
|
||||
image = getImage(picture)
|
||||
}
|
||||
}
|
||||
Box(Modifier.fillMaxWidth().height(393.dp), contentAlignment = Alignment.Center) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
.clickable { onClick() },
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = image,
|
||||
transitionSpec = {
|
||||
slideInHorizontally(
|
||||
initialOffsetX = { it }, animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioLowBouncy,
|
||||
stiffness = Spring.StiffnessLow
|
||||
)
|
||||
) with slideOutHorizontally(
|
||||
targetOffsetX = { -it }, animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioLowBouncy,
|
||||
stiffness = Spring.StiffnessLow
|
||||
)
|
||||
)
|
||||
// slideInVertically(initialOffsetY = { it }) with slideOutVertically(targetOffsetY = { -it })
|
||||
}
|
||||
) { imageBitmap ->
|
||||
if (imageBitmap != null) {
|
||||
Image(
|
||||
bitmap = imageBitmap,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
} else {
|
||||
Spacer(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier.height(5.dp).fillMaxWidth()
|
||||
.background(brush = kotlinHorizontalGradientBrush)
|
||||
)
|
||||
Card(
|
||||
modifier = Modifier.height(200.dp)
|
||||
.background(brush = kotlinHorizontalGradientBrush)
|
||||
.padding(10.dp)
|
||||
.clickable { onClick() },
|
||||
shape = RoundedCornerShape(10.dp, 10.dp, 10.dp, 10.dp),
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = image,
|
||||
transitionSpec = {
|
||||
slideInVertically(initialOffsetY = { it }) with slideOutVertically(targetOffsetY = { -it })
|
||||
}
|
||||
) { imageBitmap ->
|
||||
if (imageBitmap != null) {
|
||||
Image(
|
||||
bitmap = imageBitmap,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
,
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
} else {
|
||||
Spacer(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
.background(brush = kotlinHorizontalGradientBrush)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.3 KiB |
Reference in New Issue
Block a user