forked from melod1n/fast-messenger
Refactor: Improve swipe-to-dismiss and image sharing
This commit refactors the swipe-to-dismiss gesture and the image sharing logic in the photo viewer. The swipe-to-dismiss animation is now smoother and more reliable, using `Animatable` instead of `animateFloatAsState`. The background dimming effect has also been improved to be more responsive to the drag gesture. Additionally, the responsibility for creating the share `Intent` has been moved from the composable screen into the `PhotoViewViewModel`, improving the separation of concerns. Key changes: - Replaced `animateFloatAsState` with `Animatable` for smoother swipe-to-dismiss animations. - Improved the alpha calculation for the background during the drag gesture. - Moved the creation of the share `Intent` into the `PhotoViewViewModel`. - Simplified the drag-handling logic by removing local state management from the composable.
This commit is contained in:
+13
-3
@@ -35,7 +35,7 @@ import dev.meloda.fast.ui.R
|
|||||||
interface PhotoViewViewModel {
|
interface PhotoViewViewModel {
|
||||||
val screenState: StateFlow<PhotoViewScreenState>
|
val screenState: StateFlow<PhotoViewScreenState>
|
||||||
|
|
||||||
val shareRequest: StateFlow<Uri?>
|
val shareRequest: StateFlow<Intent?>
|
||||||
|
|
||||||
fun onPageChanged(newPage: Int)
|
fun onPageChanged(newPage: Int)
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ class PhotoViewViewModelImpl(
|
|||||||
)
|
)
|
||||||
|
|
||||||
override val screenState = MutableStateFlow(PhotoViewScreenState.EMPTY)
|
override val screenState = MutableStateFlow(PhotoViewScreenState.EMPTY)
|
||||||
override val shareRequest = MutableStateFlow<Uri?>(null)
|
override val shareRequest = MutableStateFlow<Intent?>(null)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
screenState.setValue { old ->
|
screenState.setValue { old ->
|
||||||
@@ -94,7 +94,17 @@ class PhotoViewViewModelImpl(
|
|||||||
imageFile
|
imageFile
|
||||||
)
|
)
|
||||||
|
|
||||||
shareRequest.setValue { uri }
|
shareRequest.setValue {
|
||||||
|
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||||
|
type = "image/png"
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
putExtra(Intent.EXTRA_STREAM, uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
val chooserIntent = Intent.createChooser(intent, null)
|
||||||
|
chooserIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
chooserIntent
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+48
-64
@@ -3,9 +3,7 @@ package dev.meloda.fast.photoviewer.presentation
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.LinearEasing
|
import androidx.compose.animation.core.Animatable
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
|
||||||
import androidx.compose.animation.core.tween
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
@@ -32,7 +30,6 @@ import androidx.compose.material3.TopAppBar
|
|||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -64,6 +61,7 @@ import dev.meloda.fast.ui.components.FullScreenDialog
|
|||||||
import dev.meloda.fast.ui.components.Loader
|
import dev.meloda.fast.ui.components.Loader
|
||||||
import dev.meloda.fast.ui.util.getImage
|
import dev.meloda.fast.ui.util.getImage
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
@@ -108,26 +106,17 @@ private fun PhotoViewRoute(
|
|||||||
viewModel: PhotoViewViewModel = koinViewModel<PhotoViewViewModelImpl>()
|
viewModel: PhotoViewViewModel = koinViewModel<PhotoViewViewModelImpl>()
|
||||||
) {
|
) {
|
||||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||||
val shareRequest by viewModel.shareRequest.collectAsStateWithLifecycle()
|
val shareRequestIntent by viewModel.shareRequest.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
LaunchedEffect(shareRequest) {
|
LaunchedEffect(shareRequestIntent) {
|
||||||
if (shareRequest != null) {
|
if (shareRequestIntent!= null) {
|
||||||
viewModel.onImageShared()
|
viewModel.onImageShared()
|
||||||
|
|
||||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
|
||||||
type = "image/png"
|
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
putExtra(Intent.EXTRA_STREAM, shareRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
val chooserIntent = Intent.createChooser(intent, null)
|
|
||||||
chooserIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
context.startActivity(chooserIntent)
|
context.startActivity(shareRequestIntent)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
|
||||||
@@ -168,6 +157,8 @@ private fun PhotoViewScreen(
|
|||||||
initialPage = screenState.selectedPage
|
initialPage = screenState.selectedPage
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val windowInfo = LocalWindowInfo.current
|
||||||
|
|
||||||
LaunchedEffect(pagerState) {
|
LaunchedEffect(pagerState) {
|
||||||
snapshotFlow { pagerState.currentPage }
|
snapshotFlow { pagerState.currentPage }
|
||||||
.collect(onPageChanged)
|
.collect(onPageChanged)
|
||||||
@@ -175,15 +166,14 @@ private fun PhotoViewScreen(
|
|||||||
|
|
||||||
var offsetY by remember { mutableFloatStateOf(0f) }
|
var offsetY by remember { mutableFloatStateOf(0f) }
|
||||||
|
|
||||||
val calculatedAlpha by remember(offsetY) {
|
val alpha by snapshotFlow {
|
||||||
derivedStateOf {
|
if (offsetY == 0f) {
|
||||||
val absoluteOffset = abs(offsetY)
|
1f
|
||||||
|
} else {
|
||||||
1 - if (absoluteOffset >= 1700) {
|
(windowInfo.containerSize.width.toFloat() / (abs(offsetY) * 4))
|
||||||
0.85f
|
.coerceIn(0f, 1f)
|
||||||
} else absoluteOffset / 2000
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}.collectAsStateWithLifecycle(1f)
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -195,7 +185,7 @@ private fun PhotoViewScreen(
|
|||||||
onCopyLinkClicked = onCopyLinkClicked,
|
onCopyLinkClicked = onCopyLinkClicked,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
containerColor = Color.Black.copy(alpha = calculatedAlpha)
|
containerColor = Color.Black.copy(alpha = alpha)
|
||||||
) { padding ->
|
) { padding ->
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
Pager(
|
Pager(
|
||||||
@@ -329,6 +319,36 @@ private fun Pager(
|
|||||||
) {
|
) {
|
||||||
val windowInfo = LocalWindowInfo.current
|
val windowInfo = LocalWindowInfo.current
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val offsetY = remember { Animatable(0f) }
|
||||||
|
LaunchedEffect(offsetY.value) {
|
||||||
|
onVerticalDrag(offsetY.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
val imageModifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.graphicsLayer {
|
||||||
|
this.translationY = offsetY.value
|
||||||
|
}
|
||||||
|
.draggable(
|
||||||
|
state = rememberDraggableState { delta ->
|
||||||
|
scope.launch {
|
||||||
|
offsetY.snapTo(offsetY.value + delta)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
orientation = Orientation.Vertical,
|
||||||
|
onDragStopped = {
|
||||||
|
if (abs(offsetY.value) / windowInfo.containerSize.height >= 0.25) {
|
||||||
|
onBack()
|
||||||
|
} else {
|
||||||
|
scope.launch {
|
||||||
|
offsetY.animateTo(0f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
modifier = modifier.fillMaxSize()
|
modifier = modifier.fillMaxSize()
|
||||||
@@ -344,49 +364,13 @@ private fun Pager(
|
|||||||
Image(
|
Image(
|
||||||
painter = model,
|
painter = model,
|
||||||
contentDescription = "Image",
|
contentDescription = "Image",
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = imageModifier
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
var offsetY by remember { mutableFloatStateOf(0f) }
|
|
||||||
|
|
||||||
var useAnimatedOffset by remember {
|
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
val animatedOffset by animateFloatAsState(
|
|
||||||
targetValue = offsetY,
|
|
||||||
label = "animatedOffset",
|
|
||||||
animationSpec = tween(
|
|
||||||
durationMillis = if (useAnimatedOffset) 150 else 0,
|
|
||||||
easing = LinearEasing
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = model,
|
model = model,
|
||||||
contentDescription = "Image",
|
contentDescription = "Image",
|
||||||
modifier = Modifier
|
modifier = imageModifier,
|
||||||
.graphicsLayer {
|
|
||||||
this.translationY = animatedOffset
|
|
||||||
}
|
|
||||||
.draggable(
|
|
||||||
state = rememberDraggableState { delta ->
|
|
||||||
useAnimatedOffset = false
|
|
||||||
offsetY += delta
|
|
||||||
onVerticalDrag(offsetY)
|
|
||||||
},
|
|
||||||
orientation = Orientation.Vertical,
|
|
||||||
onDragStopped = {
|
|
||||||
if (abs(offsetY) / windowInfo.containerSize.height >= 0.25) {
|
|
||||||
onBack()
|
|
||||||
} else {
|
|
||||||
useAnimatedOffset = true
|
|
||||||
offsetY = 0f
|
|
||||||
onVerticalDrag(0f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.fillMaxSize(),
|
|
||||||
placeholder = ColorPainter(Color.DarkGray),
|
placeholder = ColorPainter(Color.DarkGray),
|
||||||
error = ColorPainter(Color.Red)
|
error = ColorPainter(Color.Red)
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user