mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-05-25 02:05:40 +00:00
android: Image gallery
This commit is contained in:
@@ -112,6 +112,7 @@ dependencies {
|
||||
|
||||
//Camera Permission
|
||||
implementation "com.google.accompanist:accompanist-permissions:0.23.0"
|
||||
implementation "com.google.accompanist:accompanist-pager:0.25.1"
|
||||
|
||||
// Link Previews
|
||||
implementation 'org.jsoup:jsoup:1.13.1'
|
||||
|
||||
@@ -33,8 +33,7 @@ import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.call.*
|
||||
import chat.simplex.app.views.chat.group.*
|
||||
import chat.simplex.app.views.chat.item.ChatItemView
|
||||
import chat.simplex.app.views.chat.item.ItemAction
|
||||
import chat.simplex.app.views.chat.item.*
|
||||
import chat.simplex.app.views.chatlist.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.helpers.AppBarHeight
|
||||
@@ -470,6 +469,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
|
||||
Spacer(Modifier.size(8.dp))
|
||||
val reversedChatItems by remember { derivedStateOf { chatItems.reversed() } }
|
||||
val maxHeightRounded = with(LocalDensity.current) { maxHeight.roundToPx() }
|
||||
LazyColumn(Modifier.align(Alignment.BottomCenter), state = listState, reverseLayout = true) {
|
||||
itemsIndexed(reversedChatItems) { i, cItem ->
|
||||
CompositionLocalProvider(
|
||||
@@ -497,7 +497,19 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val provider = remember(chatItems) {
|
||||
if (cItem.content.msgContent is MsgContent.MCImage) {
|
||||
val itemsWithImages by lazy { chatItems.filter { it.content.msgContent is MsgContent.MCImage && it.file?.loaded == true } }
|
||||
ImageGalleryProvider.from(cItem.id, { itemsWithImages }) { dismissedIndex ->
|
||||
scope.launch {
|
||||
listState.scrollToItem(
|
||||
kotlin.math.min(reversedChatItems.lastIndex, reversedChatItems.indexOfFirst { it.id == itemsWithImages[dismissedIndex].id } + 1),
|
||||
-maxHeightRounded / 2
|
||||
)
|
||||
}
|
||||
}
|
||||
} else null
|
||||
}
|
||||
if (chat.chatInfo is ChatInfo.Group) {
|
||||
if (cItem.chatDir is CIDirection.GroupRcv) {
|
||||
val prevItem = if (i < reversedChatItems.lastIndex) reversedChatItems[i + 1] else null
|
||||
@@ -523,11 +535,11 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
} else {
|
||||
Spacer(Modifier.size(42.dp))
|
||||
}
|
||||
ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, showMember = showMember, chatModelIncognito = chatModelIncognito, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall)
|
||||
ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, provider, showMember = showMember, chatModelIncognito = chatModelIncognito, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall)
|
||||
}
|
||||
} else {
|
||||
Box(Modifier.padding(start = 86.dp, end = 12.dp).then(swipeableModifier)) {
|
||||
ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, chatModelIncognito = chatModelIncognito, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall)
|
||||
ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, provider, chatModelIncognito = chatModelIncognito, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall)
|
||||
}
|
||||
}
|
||||
} else { // direct message
|
||||
@@ -538,7 +550,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
end = if (sent) 12.dp else 76.dp,
|
||||
).then(swipeableModifier)
|
||||
) {
|
||||
ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, chatModelIncognito = chatModelIncognito, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = joinGroup, acceptCall = acceptCall)
|
||||
ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, provider, chatModelIncognito = chatModelIncognito, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = joinGroup, acceptCall = acceptCall)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Download
|
||||
import androidx.compose.material.icons.outlined.ArrowDownward
|
||||
import androidx.compose.material.icons.outlined.MoreHoriz
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -27,6 +26,8 @@ import chat.simplex.app.BuildConfig
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.CIFile
|
||||
import chat.simplex.app.model.CIFileStatus
|
||||
import chat.simplex.app.views.chat.item.ImageFullScreenView
|
||||
import chat.simplex.app.views.chat.item.ImageGalleryProvider
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import coil.ImageLoader
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
@@ -39,6 +40,7 @@ import java.io.File
|
||||
fun CIImageView(
|
||||
image: String,
|
||||
file: CIFile?,
|
||||
provider: ImageGalleryProvider,
|
||||
showMenu: MutableState<Boolean>,
|
||||
receiveFile: (Long) -> Unit
|
||||
) {
|
||||
@@ -154,7 +156,9 @@ fun CIImageView(
|
||||
)
|
||||
imageView(imagePainter, onClick = {
|
||||
if (getLoadedFilePath(context, file) != null) {
|
||||
ModalManager.shared.showCustomModal { close -> ImageFullScreenView(imageBitmap, uri, close) }
|
||||
ModalManager.shared.showCustomModal(animated = false) { close ->
|
||||
ImageFullScreenView(provider, close)
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
|
||||
@@ -34,6 +34,7 @@ fun ChatItemView(
|
||||
composeState: MutableState<ComposeState>,
|
||||
cxt: Context,
|
||||
uriHandler: UriHandler? = null,
|
||||
imageProvider: ImageGalleryProvider? = null,
|
||||
showMember: Boolean = false,
|
||||
chatModelIncognito: Boolean,
|
||||
useLinkPreviews: Boolean,
|
||||
@@ -63,7 +64,7 @@ fun ChatItemView(
|
||||
EmojiItemView(cItem)
|
||||
} else {
|
||||
val onLinkLongClick = { _: String -> showMenu.value = true }
|
||||
FramedItemView(cInfo, cItem, uriHandler, showMember = showMember, showMenu, receiveFile, onLinkLongClick)
|
||||
FramedItemView(cInfo, cItem, uriHandler, imageProvider, showMember = showMember, showMenu, receiveFile, onLinkLongClick)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = showMenu.value,
|
||||
|
||||
@@ -37,6 +37,7 @@ fun FramedItemView(
|
||||
chatInfo: ChatInfo,
|
||||
ci: ChatItem,
|
||||
uriHandler: UriHandler? = null,
|
||||
imageProvider: ImageGalleryProvider? = null,
|
||||
showMember: Boolean = false,
|
||||
showMenu: MutableState<Boolean>,
|
||||
receiveFile: (Long) -> Unit,
|
||||
@@ -126,7 +127,7 @@ fun FramedItemView(
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
when (val mc = ci.content.msgContent) {
|
||||
is MsgContent.MCImage -> {
|
||||
CIImageView(image = mc.image, file = ci.file, showMenu, receiveFile)
|
||||
CIImageView(image = mc.image, file = ci.file, imageProvider ?: return@Box, showMenu, receiveFile)
|
||||
if (mc.text == "") {
|
||||
metaColor = Color.White
|
||||
} else {
|
||||
|
||||
+103
-55
@@ -1,80 +1,128 @@
|
||||
package chat.simplex.app.views.chat.item
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.gestures.detectTransformGestures
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.input.pointer.*
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.core.content.FileProvider
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ChatItem
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import coil.ImageLoader
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import coil.decode.GifDecoder
|
||||
import coil.decode.ImageDecoderDecoder
|
||||
import coil.request.ImageRequest
|
||||
import coil.size.Size
|
||||
import com.google.accompanist.pager.*
|
||||
import java.io.File
|
||||
|
||||
@Composable
|
||||
fun ImageFullScreenView(imageBitmap: Bitmap, uri: Uri, close: () -> Unit) {
|
||||
BackHandler(onBack = close)
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black)
|
||||
.clickable(onClick = close)
|
||||
) {
|
||||
var scale by remember { mutableStateOf(1f) }
|
||||
var translationX by remember { mutableStateOf(0f) }
|
||||
var translationY by remember { mutableStateOf(0f) }
|
||||
// I'm making a new instance of imageLoader here because if I use one instance in multiple places
|
||||
// after end of composition here a GIF from the first instance will be paused automatically which isn't what I want
|
||||
val imageLoader = ImageLoader.Builder(LocalContext.current)
|
||||
.components {
|
||||
if (Build.VERSION.SDK_INT >= 28) {
|
||||
add(ImageDecoderDecoder.Factory())
|
||||
} else {
|
||||
add(GifDecoder.Factory())
|
||||
}
|
||||
interface ImageGalleryProvider {
|
||||
val currentItem: Int
|
||||
val totalImagesSize: Int
|
||||
fun uniqueKey(index: Int): Long
|
||||
fun getImage(index: Int): Pair<Bitmap, Uri>?
|
||||
fun onDismiss(index: Int)
|
||||
|
||||
companion object {
|
||||
fun from(chatItemId: Long, items: () -> List<ChatItem>, onDismiss: (Int) -> Unit): ImageGalleryProvider = object: ImageGalleryProvider {
|
||||
override val currentItem: Int
|
||||
get() = items().indexOfFirst { it.id == chatItemId }
|
||||
override val totalImagesSize: Int
|
||||
get() = items().size
|
||||
|
||||
override fun uniqueKey(index: Int): Long = items()[index].id
|
||||
override fun getImage(index: Int): Pair<Bitmap, Uri>? {
|
||||
val file = items().getOrNull(index)?.file
|
||||
val imageBitmap: Bitmap? = getLoadedImage(SimplexApp.context, file)
|
||||
val filePath = getLoadedFilePath(SimplexApp.context, file)
|
||||
return if (imageBitmap != null && filePath != null) {
|
||||
val uri = FileProvider.getUriForFile(SimplexApp.context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath))
|
||||
imageBitmap to uri
|
||||
} else null
|
||||
}
|
||||
.build()
|
||||
Image(
|
||||
rememberAsyncImagePainter(
|
||||
ImageRequest.Builder(LocalContext.current).data(data = uri).size(Size.ORIGINAL).build(),
|
||||
placeholder = BitmapPainter(imageBitmap.asImageBitmap()), // show original image while it's still loading by coil
|
||||
imageLoader = imageLoader
|
||||
),
|
||||
contentDescription = stringResource(R.string.image_descr),
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier
|
||||
.graphicsLayer(
|
||||
scaleX = scale,
|
||||
scaleY = scale,
|
||||
translationX = translationX,
|
||||
translationY = translationY,
|
||||
)
|
||||
.pointerInput(Unit) {
|
||||
detectTransformGestures(
|
||||
onGesture = { _, pan, gestureZoom, _ ->
|
||||
scale = (scale * gestureZoom).coerceIn(1f, 20f)
|
||||
if (scale > 1) {
|
||||
translationX += pan.x * scale
|
||||
translationY += pan.y * scale
|
||||
} else {
|
||||
translationX = 0f
|
||||
translationY = 0f
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
.fillMaxSize(),
|
||||
)
|
||||
override fun onDismiss(index: Int) { onDismiss(index) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
@Composable
|
||||
fun ImageFullScreenView(provider: ImageGalleryProvider, close: () -> Unit) {
|
||||
val pagerState = rememberPagerState(provider.currentItem)
|
||||
val goBack = { provider.onDismiss(pagerState.currentPage); close() }
|
||||
BackHandler(onBack = goBack)
|
||||
HorizontalPager(count = provider.totalImagesSize, state = pagerState, key = { provider.uniqueKey(it) }) { index ->
|
||||
val (imageBitmap: Bitmap, uri: Uri) = provider.getImage(index) ?: return@HorizontalPager
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black)
|
||||
.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = goBack)
|
||||
) {
|
||||
var scale by remember { mutableStateOf(1f) }
|
||||
var translationX by remember { mutableStateOf(0f) }
|
||||
var translationY by remember { mutableStateOf(0f) }
|
||||
LaunchedEffect(pagerState.currentPage) {
|
||||
scale = 1f
|
||||
translationX = 0f
|
||||
translationY = 0f
|
||||
}
|
||||
// I'm making a new instance of imageLoader here because if I use one instance in multiple places
|
||||
// after end of composition here a GIF from the first instance will be paused automatically which isn't what I want
|
||||
val imageLoader = ImageLoader.Builder(LocalContext.current)
|
||||
.components {
|
||||
if (Build.VERSION.SDK_INT >= 28) {
|
||||
add(ImageDecoderDecoder.Factory())
|
||||
} else {
|
||||
add(GifDecoder.Factory())
|
||||
}
|
||||
}
|
||||
.build()
|
||||
Image(
|
||||
rememberAsyncImagePainter(
|
||||
ImageRequest.Builder(LocalContext.current).data(data = uri).size(Size.ORIGINAL).build(),
|
||||
placeholder = BitmapPainter(imageBitmap.asImageBitmap()), // show original image while it's still loading by coil
|
||||
imageLoader = imageLoader
|
||||
),
|
||||
contentDescription = stringResource(R.string.image_descr),
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier
|
||||
.graphicsLayer(
|
||||
scaleX = scale,
|
||||
scaleY = scale,
|
||||
translationX = translationX,
|
||||
translationY = translationY,
|
||||
)
|
||||
.pointerInput(Unit) {
|
||||
detectTransformGestures (
|
||||
onGesture = { _, pan, gestureZoom, _ ->
|
||||
scale = (scale * gestureZoom).coerceIn(1f, 20f)
|
||||
if (scale > 1) {
|
||||
translationX += pan.x * scale
|
||||
translationY += pan.y * scale
|
||||
} else {
|
||||
translationX = 0f
|
||||
translationY = 0f
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,24 +19,13 @@ package chat.simplex.app.views.helpers
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.gestures.forEachGesture
|
||||
import androidx.compose.foundation.gestures.*
|
||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||
import androidx.compose.foundation.interaction.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.pointer.AwaitPointerEventScope
|
||||
import androidx.compose.ui.input.pointer.PointerEventTimeoutCancellationException
|
||||
import androidx.compose.ui.input.pointer.PointerEvent
|
||||
import androidx.compose.ui.input.pointer.PointerEventPass
|
||||
import androidx.compose.ui.input.pointer.PointerInputChange
|
||||
import androidx.compose.ui.input.pointer.PointerInputScope
|
||||
import androidx.compose.ui.input.pointer.changedToDown
|
||||
import androidx.compose.ui.input.pointer.changedToDownIgnoreConsumed
|
||||
import androidx.compose.ui.input.pointer.changedToUp
|
||||
import androidx.compose.ui.input.pointer.consumeAllChanges
|
||||
import androidx.compose.ui.input.pointer.consumeDownChange
|
||||
import androidx.compose.ui.input.pointer.isOutOfBounds
|
||||
import androidx.compose.ui.input.pointer.positionChangeConsumed
|
||||
import androidx.compose.ui.input.pointer.*
|
||||
import androidx.compose.ui.platform.LocalViewConfiguration
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.util.fastAll
|
||||
@@ -45,6 +34,8 @@ import androidx.compose.ui.util.fastForEach
|
||||
import chat.simplex.app.TAG
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* See original code here: [androidx.compose.foundation.gestures.detectTapGestures]
|
||||
@@ -221,3 +212,65 @@ fun interactionSourceWithDetection(onClick: () -> Unit, onLongClick: () -> Unit)
|
||||
}
|
||||
return interactionSource
|
||||
}
|
||||
|
||||
suspend fun PointerInputScope.detectTransformGestures(
|
||||
panZoomLock: Boolean = false,
|
||||
onGesture: (centroid: Offset, pan: Offset, zoom: Float, rotation: Float) -> Unit
|
||||
) {
|
||||
forEachGesture {
|
||||
awaitPointerEventScope {
|
||||
var rotation = 0f
|
||||
var zoom = 1f
|
||||
var pan = Offset.Zero
|
||||
var pastTouchSlop = false
|
||||
val touchSlop = viewConfiguration.touchSlop
|
||||
var lockedToPanZoom = false
|
||||
|
||||
awaitFirstDown(requireUnconsumed = false)
|
||||
do {
|
||||
val event = awaitPointerEvent()
|
||||
val canceled = event.changes.fastAny { it.isConsumed }
|
||||
if (!canceled) {
|
||||
val zoomChange = event.calculateZoom()
|
||||
val rotationChange = event.calculateRotation()
|
||||
val panChange = event.calculatePan()
|
||||
|
||||
if (!pastTouchSlop) {
|
||||
zoom *= zoomChange
|
||||
rotation += rotationChange
|
||||
pan += panChange
|
||||
|
||||
val centroidSize = event.calculateCentroidSize(useCurrent = false)
|
||||
val zoomMotion = abs(1 - zoom) * centroidSize
|
||||
val rotationMotion = abs(rotation * PI.toFloat() * centroidSize / 180f)
|
||||
val panMotion = pan.getDistance()
|
||||
|
||||
if (zoomMotion > touchSlop ||
|
||||
rotationMotion > touchSlop ||
|
||||
panMotion > touchSlop
|
||||
) {
|
||||
pastTouchSlop = true
|
||||
lockedToPanZoom = panZoomLock && rotationMotion < touchSlop
|
||||
}
|
||||
}
|
||||
|
||||
if (pastTouchSlop) {
|
||||
val centroid = event.calculateCentroid(useCurrent = false)
|
||||
val effectiveRotation = if (lockedToPanZoom) 0f else rotationChange
|
||||
if (effectiveRotation != 0f ||
|
||||
zoomChange != 1f ||
|
||||
panChange != Offset.Zero
|
||||
) {
|
||||
onGesture(centroid, panChange, zoomChange, effectiveRotation)
|
||||
}
|
||||
event.changes.fastForEach {
|
||||
if (it.positionChanged() && zoomChange != 1f) {
|
||||
it.consume()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (!canceled && event.changes.fastAny { it.pressed })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ fun ModalView(
|
||||
}
|
||||
|
||||
class ModalManager {
|
||||
private val modalViews = arrayListOf<(@Composable (close: () -> Unit) -> Unit)?>()
|
||||
private val modalViews = arrayListOf<Pair<Boolean, (@Composable (close: () -> Unit) -> Unit)>>()
|
||||
private val modalCount = mutableStateOf(0)
|
||||
private val toRemove = mutableSetOf<Int>()
|
||||
private var oldViewChanging = AtomicBoolean(false)
|
||||
@@ -49,21 +49,21 @@ class ModalManager {
|
||||
}
|
||||
}
|
||||
|
||||
fun showCustomModal(modal: @Composable (close: () -> Unit) -> Unit) {
|
||||
fun showCustomModal(animated: Boolean = true, modal: @Composable (close: () -> Unit) -> Unit) {
|
||||
Log.d(TAG, "ModalManager.showModal")
|
||||
// Means, animation is in progress or not started yet. Do not wait until animation finishes, just remove all from screen.
|
||||
// This is useful when invoking close() and ShowCustomModal one after another without delay. Otherwise, screen will hold prev view
|
||||
if (toRemove.isNotEmpty()) {
|
||||
runAtomically { toRemove.removeIf { elem -> modalViews.removeAt(elem); true } }
|
||||
}
|
||||
modalViews.add(modal)
|
||||
modalViews.add(animated to modal)
|
||||
modalCount.value = modalViews.size - toRemove.size
|
||||
}
|
||||
|
||||
fun closeModal() {
|
||||
if (modalViews.isNotEmpty()) {
|
||||
//modalViews.removeAt(modalViews.lastIndex)
|
||||
runAtomically { toRemove.add(modalViews.lastIndex - toRemove.size) }
|
||||
if (modalViews.lastOrNull()?.first == false) modalViews.removeAt(modalViews.lastIndex)
|
||||
else runAtomically { toRemove.add(modalViews.lastIndex - toRemove.size) }
|
||||
}
|
||||
modalCount.value = modalViews.size - toRemove.size
|
||||
}
|
||||
@@ -75,6 +75,11 @@ class ModalManager {
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
fun showInView() {
|
||||
// Without animation
|
||||
if (modalCount.value > 0 && modalViews.lastOrNull()?.first == false) {
|
||||
modalViews.lastOrNull()?.second?.invoke(::closeModal)
|
||||
return
|
||||
}
|
||||
AnimatedContent(targetState = modalCount.value,
|
||||
transitionSpec = {
|
||||
if (targetState > initialState) {
|
||||
@@ -84,7 +89,7 @@ class ModalManager {
|
||||
}.using(SizeTransform(clip = false))
|
||||
}
|
||||
) {
|
||||
modalViews.getOrNull(it - 1)?.invoke(::closeModal)
|
||||
modalViews.getOrNull(it - 1)?.second?.invoke(::closeModal)
|
||||
// This is needed because if we delete from modalViews immediately on request, animation will be bad
|
||||
if (toRemove.isNotEmpty() && it == modalCount.value && transition.currentState == EnterExitState.Visible && !transition.isRunning) {
|
||||
runAtomically { toRemove.removeIf { elem -> modalViews.removeAt(elem); true } }
|
||||
|
||||
Reference in New Issue
Block a user