From 6655e74584df681cd5b4f09da40afd01f6c84f58 Mon Sep 17 00:00:00 2001 From: Prasidh Gopal Anchan Date: Fri, 4 Oct 2024 18:59:18 +0530 Subject: [PATCH] feat: add pinch to zoom and pan functionality to posts --- .../main/java/com/mca/ui/component/CMPager.kt | 45 ++++++++++++++-- .../com/mca/util/constant/UtilAnimations.kt | 19 +++++-- .../mca/home/component/AnnouncementCard.kt | 51 +++++++++++++++---- 3 files changed, 96 insertions(+), 19 deletions(-) diff --git a/core/ui/src/main/java/com/mca/ui/component/CMPager.kt b/core/ui/src/main/java/com/mca/ui/component/CMPager.kt index 2212a58..e07153b 100644 --- a/core/ui/src/main/java/com/mca/ui/component/CMPager.kt +++ b/core/ui/src/main/java/com/mca/ui/component/CMPager.kt @@ -15,6 +15,8 @@ package com.mca.ui.component import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.rememberTransformableState +import androidx.compose.foundation.gestures.transformable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -23,6 +25,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight @@ -34,24 +37,34 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Surface import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +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.draw.clip +import androidx.compose.ui.draw.scale +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.mca.ui.R import com.mca.ui.theme.Black import com.mca.ui.theme.LightBlack import com.mca.ui.theme.tintColor +import com.mca.util.constant.animateAlpha /** * Horizontal pager composable to display post images along with the dots indicator. + * Includes additional features such as pinch to zoom and pan. * @param images List of image URLs to be displayed in the pager. * @param state PagerState to control and observe the pager's state. * @param modifier Modifier for styling and layout customization. @@ -72,8 +85,24 @@ fun CMPager( enableRemoveIcon: Boolean = false, enableClick: Boolean = false, onClick: () -> Unit = { }, - onRemoveImageClick: (image: String) -> Unit = { } + onRemoveImageClick: (image: String) -> Unit = { }, + onTransform: (Boolean) -> Unit = { } ) { + var scale by remember { mutableFloatStateOf(1f) } + var offset by remember { mutableStateOf(Offset.Zero) } + val transformable = rememberTransformableState { zoomChange, panChange, _ -> + scale = (scale * zoomChange).coerceAtLeast(1f) + if (scale != 1f) offset = (offset + panChange) + } + + LaunchedEffect(key1 = transformable.isTransformInProgress) { + if (!transformable.isTransformInProgress) { + scale = 1f + offset = Offset.Zero + } + if (transformable.isTransformInProgress) onTransform(true) else onTransform(false) + } + Column( modifier = Modifier .fillMaxWidth() @@ -83,7 +112,7 @@ fun CMPager( ) { Surface( modifier = modifier - .padding(top = 4.dp, bottom = 10.dp) + .padding(bottom = 10.dp) .fillMaxWidth() .height(220.dp) .clickable( @@ -91,7 +120,10 @@ fun CMPager( indication = null, interactionSource = remember(::MutableInteractionSource), onClick = onClick - ), + ) + .transformable(transformable) + .scale(scale) + .offset { IntOffset(offset.x.toInt(), offset.y.toInt()) }, shape = RoundedCornerShape(10.dp), color = LightBlack ) { @@ -138,7 +170,12 @@ fun CMPager( PagerDot( pageCount = state.pageCount, - selectedPage = state.currentPage + selectedPage = state.currentPage, + modifier = Modifier.animateAlpha( + delay = 0, + duration = 250, + condition = !transformable.isTransformInProgress + ) ) } } diff --git a/core/util/src/main/java/com/mca/util/constant/UtilAnimations.kt b/core/util/src/main/java/com/mca/util/constant/UtilAnimations.kt index 3e2c80f..fa816ac 100644 --- a/core/util/src/main/java/com/mca/util/constant/UtilAnimations.kt +++ b/core/util/src/main/java/com/mca/util/constant/UtilAnimations.kt @@ -67,19 +67,30 @@ fun Modifier.animatedLike(onClick: () -> Unit) = composed { /** * Animate the alpha of any composable function such as a Card. * @param delay The delay of the enter animation in milliseconds. + * @param duration The duration of the enter animation in milliseconds. + * @param condition The condition when the composable should be shown. */ -fun Modifier.animateAlpha(delay: Int) = composed { +fun Modifier.animateAlpha( + delay: Int, + duration: Int = 800, + condition: Boolean = true +) = composed { var alpha by rememberSaveable { mutableFloatStateOf(0f) } val animatedAlpha by animateFloatAsState( targetValue = alpha, animationSpec = tween( - durationMillis = 800, + durationMillis = duration, delayMillis = delay ), label = "animatedNotificationAlpha" ) - alpha(animatedAlpha) - .onGloballyPositioned { alpha = 1f } + if (condition) { + alpha(animatedAlpha) + .onGloballyPositioned { alpha = 1f } + } else { + alpha(animatedAlpha) + .onGloballyPositioned { alpha = 0f } + } } /** diff --git a/feature/home/src/main/java/com/mca/home/component/AnnouncementCard.kt b/feature/home/src/main/java/com/mca/home/component/AnnouncementCard.kt index 96cd67b..372395b 100644 --- a/feature/home/src/main/java/com/mca/home/component/AnnouncementCard.kt +++ b/feature/home/src/main/java/com/mca/home/component/AnnouncementCard.kt @@ -36,6 +36,7 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -44,6 +45,7 @@ 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.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle @@ -63,6 +65,7 @@ import com.mca.ui.theme.dosis import com.mca.ui.theme.fontColor import com.mca.ui.theme.tintColor import com.mca.util.constant.Constant.ADMIN +import com.mca.util.constant.animateAlpha import com.mca.util.constant.animatedLike import com.mca.util.constant.toLikedBy import com.mca.util.constant.toLikes @@ -83,6 +86,8 @@ internal fun AnnouncementCard( onLikeClick: (postId: String, token: String) -> Unit, onUnlikeCLick: (postId: String) -> Unit ) { + var alpha by remember { mutableFloatStateOf(1f) } + Column( modifier = modifier .animateContentSize(animationSpec = tween(durationMillis = 400)) @@ -108,18 +113,29 @@ internal fun AnnouncementCard( ) { Column( modifier = modifier - .padding(all = 10.dp) + .padding(all = 15.dp) .fillMaxWidth() .wrapContentHeight(Alignment.CenterVertically), verticalArrangement = Arrangement.Top, horizontalAlignment = Alignment.Start ) { - MainContent(post = post) + MainContent( + post = post, + alpha = alpha, + onTransform = { transforming -> + alpha = if (transforming) 0f else 1f + } + ) LikesAndTimeStamp( post = post, token = user.token, currentUserId = currentUserId, currentUsername = currentUsername, + modifier = Modifier.animateAlpha( + delay = 0, + duration = 250, + condition = alpha == 1f + ), onLikeCLick = onLikeClick, onUnlikeCLick = onUnlikeCLick ) @@ -194,10 +210,13 @@ private fun AnnouncementTopBar( } @Composable -private fun AnnouncementBottomBar(likes: List) { +private fun AnnouncementBottomBar( + likes: List, + modifier: Modifier = Modifier +) { Spacer(modifier = Modifier.height(20.dp)) Row( - modifier = Modifier.fillMaxWidth(), + modifier = modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { @@ -224,7 +243,9 @@ private fun AnnouncementBottomBar(likes: List) { @Composable private fun MainContent( post: Post, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + alpha: Float, + onTransform: (Boolean) -> Unit ) { val state = rememberPagerState { post.images.size } var isOpen by remember { mutableStateOf(false) } @@ -233,7 +254,9 @@ private fun MainContent( CMPager( images = post.images, state = state, - modifier = modifier + modifier = modifier, + contentScale = ContentScale.Crop, + onTransform = onTransform ) } @@ -247,11 +270,17 @@ private fun MainContent( ), overflow = TextOverflow.Ellipsis, maxLines = if (!isOpen) 5 else Int.MAX_VALUE, - modifier = Modifier.clickable( - indication = null, - interactionSource = remember(::MutableInteractionSource), - onClick = { isOpen = !isOpen } - ) + modifier = Modifier + .clickable( + indication = null, + interactionSource = remember(::MutableInteractionSource), + onClick = { isOpen = !isOpen } + ) + .animateAlpha( + delay = 0, + duration = 250, + condition = alpha == 1f + ) ) }