From 2588c599ee3284feb459d319a4a4ef80a66099a7 Mon Sep 17 00:00:00 2001 From: Sebastian Aigner Date: Thu, 2 Mar 2023 21:19:36 +0100 Subject: [PATCH] First steps in implementing redesigned UI (#2817) Co-authored-by: Sebastian.Aigner --- .../example/imageviewer/style/Palette.kt | 14 +- .../imageviewer/view/FullscreenImage.kt | 111 +++++---- .../example/imageviewer/view/GalleryScreen.kt | 230 +++++++++--------- .../example/imageviewer/view/MemoryScreen.kt | 176 ++++++++------ .../imageviewer/view/PreviewImage.common.kt | 81 +++--- .../src/commonMain/resources/arrowleft.png | Bin 0 -> 4028 bytes .../shared/src/commonMain/resources/eye.png | Bin 0 -> 10042 bytes .../shared/src/commonMain/resources/plus.png | Bin 0 -> 2302 bytes .../shared/src/commonMain/resources/trash.png | Bin 0 -> 6459 bytes 9 files changed, 341 insertions(+), 271 deletions(-) create mode 100644 experimental/examples/imageviewer/shared/src/commonMain/resources/arrowleft.png create mode 100644 experimental/examples/imageviewer/shared/src/commonMain/resources/eye.png create mode 100644 experimental/examples/imageviewer/shared/src/commonMain/resources/plus.png create mode 100644 experimental/examples/imageviewer/shared/src/commonMain/resources/trash.png diff --git a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/style/Palette.kt b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/style/Palette.kt index 6e8e568402..f549d5767c 100755 --- a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/style/Palette.kt +++ b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/style/Palette.kt @@ -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() diff --git a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/FullscreenImage.kt b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/FullscreenImage.kt index 043e51c672..7cf4a0ae4c 100644 --- a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/FullscreenImage.kt +++ b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/FullscreenImage.kt @@ -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 + ) + } + } + 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 + } + }) } - .addUserInput(scalableState) - ) { - Image( - modifier = Modifier.fillMaxSize(), - painter = BitmapPainter( - imageWithFilter, - srcOffset = visiblePartOfImage.topLeft, - srcSize = visiblePartOfImage.size - ), - contentDescription = null - ) + } } } 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, + selectedFilters: Set, + 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) diff --git a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/GalleryScreen.kt b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/GalleryScreen.kt index a19c67304f..5f3d4fccfc 100755 --- a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/GalleryScreen.kt +++ b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/GalleryScreen.kt @@ -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) - }) - } - when (galleryPage.galleryStyle) { - GalleryStyle.SQUARES -> SquaresGalleryView( - pictures, - galleryPage.pictureId, - onSelect = { galleryPage.selectPicture(it) }, - onMakeNewMemory + 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 ) + } + 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, 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() } } }) } diff --git a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/MemoryScreen.kt b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/MemoryScreen.kt index 6603ac9b78..3a342d6237 100644 --- a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/MemoryScreen.kt +++ b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/MemoryScreen.kt @@ -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) }) + } } \ No newline at end of file diff --git a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/PreviewImage.common.kt b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/PreviewImage.common.kt index 995ffa86d9..07ab3c85b0 100644 --- a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/PreviewImage.common.kt +++ b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/PreviewImage.common.kt @@ -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 diff --git a/experimental/examples/imageviewer/shared/src/commonMain/resources/arrowleft.png b/experimental/examples/imageviewer/shared/src/commonMain/resources/arrowleft.png new file mode 100644 index 0000000000000000000000000000000000000000..4554dc244dcc4b5a512be5a09b89008178bdce6d GIT binary patch literal 4028 zcmY*c3se)=*8XNPl7v?RsTvjKC8GG^N)$vZ1NZ%y}$3< zd(L?`Y3?i!*TJp;JmTY~Cj%@EnT-k;G96!ku#Su}GcGj?aMMWt42)%3X-QV{tQZ`> z;?qG7-0JAWXyCKmZnhQu0M-2X>Cq{<%%h(k%wKx5e?Ln@u`JVN1UFe$#_ZxF*k~?r zR%itK{@J3!*rHfJ*)NJ8+TPh(xy{sZKCkTjvxOU;rZm_Ne0}ok-_Ll7cPH&{*x0+p zQ%LCZ77sOA97Si&R9;PEP01|gjP!8q{#9pl_@KWA`hfIqhw;o~*2EOJT2|6S$(iFa zlNXCJKM%XD%JIy4N+B2&$UJn*Ixo#Jx1tMcc6h?)psvrOz?90ur^-1>r;+|x$LkHM zHAYW7jBoO7e;0T%GSM-7rJoLFnyZ8*PdilVd{&Rc9Tor1H$?IN zF#n4dnZLXz?|cjVmyMkctszQ0#^H2|(VnxSL1kH(AY=!7IJPu7B9XfXe0V}rRExD< zfj7O;{?>jwwtvT%HX6xlu*O&!CBDJIxNv!U)_?C#3hb4eVX& zf>|^_lO85Uh4(RYhr`TeOlkPWz_mN81&d5U)VWC%V3_G&?!cc%y33#)G@b zzk4>S=1T9@AA^1Xi-q4=<%>gFyT~S}uHBTv>ldmBi4@^O2GfZmyl!_el?v z5+fHB}^a#p@yGqN0AgVU@SKo8Er~>?+`n- zdc%bTLDCRu-9MdJxX+R6{Ro4p8n`4G?P-l|6X3@QkIBpOuB;|s;Rz?~O6`(a#wG~m z3FjOEb!NE`uE0wknWG#iogjrNTVPeNcB%JOoeFrpadh{OWy zIw&)TN;U|(1aiDU0gpzj+>#QBo$==+HM%0A5axGI+}r2dlhG^7oDjw6OuD|Tn^!yK zdT*9*ZS1@n+hX*^9&dz{7#!zAZA@t)l8#84An9G^l{GQx-E9&S2=hWnveDin=|bGW z2?-}D(hsr`Vj|nf342KZH7<;PM(eg|IR|O(e`$`CGO(9q6eMmV0EXFLVKCYY0|>N@ z4Oc1lML`(z4FxdqJ7zI2)w^|4eHR2*1;R!rI&AyuE2D5tja4bj+dt}1@f)(Sinik) zal*T_otb6yF@*K!F>-wXZA(L4G)no62j>lw%7Y#v=BM|z^n-4?+``cc%44S&b|qc6 zt#5J!z!c4jg#>APp{K|sG_`cfO&eM9!1IwCD-Qd@Xuocwg7y@rNXyIX+I`f3)02Z=gcmejR`oQb~pvuo+c zC(1!h#}d)-CQT9$LZEICDE&4DKGml{Y2xFIqcX`htqG7E?z5h9hh^n8xfE$e{<>b7 zIQ^(62rvDxYb9vvtXGV;GD2#~AWF>f((-m0YLASAQ`lIwAW<%1Aa!ok9t{9hqNDy6RGEYVAu8OzJb)5&jH7bH(XiQazZcD02uU3L7YwFX_w)l1yypDy zp-%hniwsW?{*cu;4^jeaBO|3q`L8@ZrLVHSk!XG9xMhn{!L<3uAhD~40b;xJm7NbC zKJ?G)8=Gg>fWMI+(XT9)@1>^yOFfJB>&Kq3asHObyuL69*%ROHC`5ghom{F`l2Aw$?xKDoZ&yMA{?sw=a*qw86cCnwx{^i(6x?g%ct z+VS89V9ZXzg?}o*f3?dKAFlVq=jl&8#f&nQi!iDDB}{1B%9vgbmJ6$|$YuPLx%W}G zs`lnxzs@DVHRDvQ9K~&kORL{bFQ%lIpy;L=!luOQOzuSk@O{P?&u1bV2IGMA)9K6f z)6TB5NZLL_##j0%@L&l!i2=|i?!pkEjU{HDsoHS65Fg#Gx&nX zSFT*Kd!hfUE8X<_tWg)Pu80K&{A&{~Bq+K!=Pq@{dsP8aK({{rbc?RtA42LvjWYrL zpoRT}%RPFOrfpy>AJX=r6|c$oMVAW08O%9)U)nfx2q74HJc z0o=Ye0~eANxpOLRDJA@KvNbrccfCmsOvxUI2}4-aEZN8S)%pXkKX~En!D%9c;_R$_ zEEB_s1B>b$CsY^9UT5JFPK+fhPI|QRG+ru=FC6A+jrMwx(~Wq+;DHZ47|fiQjoekS z5WZ7j8+qF?9nC*)3B^ZhEN>2xPvelCfZQ2MrYBy^1HYXU!VjrCOQ)UEn0yp`4#iq5 z#mT?I2`@LRVG5^KOHwgw7z-$6vm0aKB`aeragv?DSh&MJxf+DvTp7I!;agRXPz_~J^&vt7CA$})wVy~7@Ijo=uUK7F!QGN0 zg~#GJNy&SrFvM2%Tr6HZO76;_IsuCdDdFoQQ2hj45UmcQ@HY5M7>$s$%kVZ#YsUp0m6#SWgHZQDSH2G_5d7 z36D3(9#LTDO(%pt@mQ1e0z%P?6J{R%3qtWMC;Xq2YB8Dy*fiM*C7%=F5XV3Xwi+_><)BxzN5%0e+!`>@$CeVT_(9 z1l4e9-4ZG`b3!4hGISiH&s@?fZiimW;;O%2l=7qs20wHr{s%sp8r{)O_{ufte%FH$ z>`K&Kq9bSvhuZ`nU(eyq_0zP{>1;D4Z5mH@bgQ@@^wEVp-rq8*SjHHGky-N+yMC0p zM=uzZ@cEWJ{K#P!3D2~QMKB55Jd6?IBYfS>0~q0=WIgG5?Oqu*&)1ZgbNp?=Hrz4| zRUbec^Pf_wDuP&@FgZzqsvQs?5^Uekg5Ha=y-$Z?E$QUGKWauw){l-XWUmLI#^{7^ zKrm{KzQnGJovhZs!Q$9#f4_Zfxyog65%~V(Y0PYx29u|fKqL*N6715@?!xHTxoYh>q1N_qj#f5q7W9MOB=f6!jZ@pNKR$fplpgUYF+Xg|SgzmfnVh7zCR>s1j zW6?m+S#ntq4p>$~>5BhLMSVt{0cR%=N z6tHa$^a@glEsi*qY&3mLoD^$IA!fb^P@lviHo@QWx4iYi{A9J305v-iGz21a>JV1C zY$?d(au`W1er5o~0c~3g=PS@j_!Zt(qSGMZCwS78E%v$mvg7FJaY$V>yO6eB<^{cK z5+~eNpeA?-%g^HwQABdoHm|BZHK)R{?)L9N2x%o5TFv$g%l;{&G&;(uCA{>4*3k LbEhAV(ZBWoXJa#~ literal 0 HcmV?d00001 diff --git a/experimental/examples/imageviewer/shared/src/commonMain/resources/eye.png b/experimental/examples/imageviewer/shared/src/commonMain/resources/eye.png new file mode 100644 index 0000000000000000000000000000000000000000..53503d3d7fc0a502ba9005329600ae9a72c5df81 GIT binary patch literal 10042 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 IXuCxJKjjNNlmGw# literal 0 HcmV?d00001 diff --git a/experimental/examples/imageviewer/shared/src/commonMain/resources/plus.png b/experimental/examples/imageviewer/shared/src/commonMain/resources/plus.png new file mode 100644 index 0000000000000000000000000000000000000000..5a46adbc8131dc2308ba3678405d3192baafe0b9 GIT binary patch literal 2302 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7&zE~)R&4Yzkn2Hfk$L90|U1(2s1Lwnj--e zWGoJHcVbv~PUa<$!;&U>cv7h@-A}a#}$7D|z$B+ufw>KGmnF9q}FE)m7_;oSs zgm@LUh>GyeR#-H9?{gLZL;rW4Jj1Yj!|wBY_Z??g{=tsHp^t%~L4lEh!4c?+CKjN< zoIulr7#IXp7#I{h7#JKTj4Bunhtc#fQ2AoP?RR$U7R|q7?qC1-yoTpPQU3AdJB$oL zbB2HR+$H;4?g7uc<95$))%E@5WB8C?KEJxQpV99iu#6flt47Q0(W+?>SJ8hN9a!(K Vs8=+~12$I}JYD@<);T3K0RV!B3AO+L literal 0 HcmV?d00001 diff --git a/experimental/examples/imageviewer/shared/src/commonMain/resources/trash.png b/experimental/examples/imageviewer/shared/src/commonMain/resources/trash.png new file mode 100644 index 0000000000000000000000000000000000000000..67e863f04a9b4de56ad9103c3afd0e02d98b8940 GIT binary patch literal 6459 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 literal 0 HcmV?d00001