add view image screen and enable edge to edge for custom selector

This commit is contained in:
Rohit Verma 2024-11-25 23:28:11 +05:30
parent 071bffbfa8
commit a930d8eca6
5 changed files with 221 additions and 9 deletions

View file

@ -144,6 +144,7 @@
android:label="@string/result" />
<activity
android:name=".customselector.ui.selector.CustomSelectorActivity"
android:windowSoftInputMode="adjustResize"
android:configChanges="screenSize|keyboard|orientation"
android:label="@string/title_activity_custom_selector"
android:parentActivityName=".contributions.MainActivity" />

View file

@ -63,6 +63,7 @@ fun CustomSelectorScreen(
uiState: CustomSelectorState,
onEvent: (CustomSelectorEvent)-> Unit,
selectedImageIds: ()-> Set<Long>,
onViewImage: (id: Long)-> Unit,
hasPartialAccess: Boolean = false
) {
val adaptiveInfo = currentWindowAdaptiveInfo()
@ -96,6 +97,7 @@ fun CustomSelectorScreen(
selectedFolder = folder,
selectedImages = selectedImageIds,
onNavigateBack = { navigator.navigateBack() },
onViewImage = onViewImage,
onEvent = onEvent,
adaptiveInfo = adaptiveInfo,
hasPartialAccess = hasPartialAccess
@ -264,6 +266,7 @@ private fun CustomSelectorScreenPreview() {
CommonsTheme {
CustomSelectorScreen(
uiState = CustomSelectorState(),
onViewImage = { },
onEvent = { },
selectedImageIds = { emptySet() },
hasPartialAccess = true

View file

@ -68,6 +68,7 @@ fun ImagesPane(
selectedFolder: Folder,
selectedImages: () -> Set<Long>,
onNavigateBack: () -> Unit,
onViewImage: (id: Long)-> Unit,
onEvent: (CustomSelectorEvent) -> Unit,
adaptiveInfo: WindowAdaptiveInfo,
hasPartialAccess: Boolean = false
@ -128,7 +129,7 @@ fun ImagesPane(
}
LazyVerticalGrid(
columns = GridCells.Adaptive(116.dp),
columns = GridCells.Adaptive(96.dp),
modifier = Modifier
.fillMaxSize()
.imageGridDragHandler(
@ -159,6 +160,8 @@ fun ImagesPane(
onClick = {
if (uiState.inSelectionMode) {
onEvent(CustomSelectorEvent.OnImageSelection(image.id))
} else {
onViewImage(image.id)
}
},
onLongClick = {

View file

@ -0,0 +1,179 @@
package fr.free.nrw.commons.customselector.ui.screens
import androidx.compose.foundation.gestures.detectTransformGestures
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
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.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.pointer.PointerInputScope
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import coil.request.ImageRequest
import fr.free.nrw.commons.customselector.model.Image
import kotlin.math.abs
@Composable
fun ViewImageScreen(
currentImageIndex: Int,
imageList: List<Image>,
) {
var imageScale by remember { mutableFloatStateOf(1f) }
var imageOffset by remember { mutableStateOf(Offset.Zero) }
var imageSize by remember { mutableStateOf(IntSize.Zero) }
var containerSize by remember { mutableStateOf(IntSize.Zero) }
val coroutineScope = rememberCoroutineScope()
LaunchedEffect(imageSize) {
println("Image Size : $imageSize")
}
val pagerState = rememberPagerState(initialPage = currentImageIndex) { imageList.size }
val scrollConnection = object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
return if (imageScale > 1f) {
// If zoomed in, consume the scroll for panning
println("Consuming for panning...")
available
} else if (
source == NestedScrollSource.UserInput && abs(pagerState.currentPageOffsetFraction) > 1e-6
) {
println("Handling swipe gestures...")
// Handle page swipes only if the image isn't zoomed
val delta = available.x
val consumed = -pagerState.dispatchRawDelta(-delta)
Offset(consumed, 0f)
} else {
println("Just passing the as it is...")
Offset.Zero
}
}
}
HorizontalPager(
state = pagerState,
key = { imageList[it].id },
pageSpacing = 16.dp
) {
BoxWithConstraints(
modifier = Modifier
.fillMaxSize()
.onSizeChanged {
containerSize = it
},
contentAlignment = Alignment.Center
) {
// val state = rememberTransformableState { zoomChange, panChange, _ ->
// imageScale = (imageScale * zoomChange).coerceIn(1f, 7f)
//
// val imageWidth = imageSize.width * imageScale
// val imageHeight = imageSize.height * imageScale
//
// val extraWidth = (imageWidth - constraints.maxWidth).coerceAtLeast(0f)
// val extraHeight = (imageHeight - constraints.maxHeight).coerceAtLeast(0f)
//
// val maxX = extraWidth / 2
// val maxY = extraHeight / 2
//
// imageOffset = Offset(
// x = (imageOffset.x + imageScale * panChange.x).coerceIn(-maxX, maxX),
// y = (imageOffset.y + imageScale * panChange.y).coerceIn(-maxY, maxY)
// )
// }
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(imageList[it].uri)
.build(),
contentDescription = null,
modifier = Modifier
.fillMaxWidth()
.onSizeChanged { imageSize = it }
// .pointerInput(Unit) {
// detectTransformGestures { centroid, pan, zoom, _ ->
// imageScale = (imageScale * zoom).coerceIn(1f, 7f)
//
// val imageWidth = imageSize.width * imageScale
// val imageHeight = imageSize.height * imageScale
//
// val extraWidth = (imageWidth-constraints.maxWidth).coerceAtLeast(0f)
// val extraHeight = (imageHeight-constraints.maxHeight).coerceAtLeast(0f)
//
// val maxX = extraWidth / 2
// val maxY = extraHeight / 2
//
// imageOffset = Offset(
// x = (imageOffset.x + imageScale * pan.x).coerceIn(
// -maxX,
// maxX
// ),
// y = (imageOffset.y + imageScale * pan.y).coerceIn(-maxY, maxY)
// )
// }
// }
.graphicsLayer {
scaleX = imageScale
scaleY = imageScale
translationX = imageOffset.x
translationY = imageOffset.y
}
)
}
}
}
suspend fun PointerInputScope.detectDragAndZoomGestures(
onZoom: (Float) -> Unit,
onDrag: (Offset) -> Unit
) {
detectTransformGestures { _, pan, zoom, _ ->
onZoom(zoom)
onDrag(pan)
}
}
fun Offset.calculateNewOffset(
centroid: Offset,
pan: Offset,
zoom: Float,
gestureZoom: Float,
size: IntSize
): Offset {
val newScale = maxOf(1f, zoom * gestureZoom)
val newOffset = (this + centroid / zoom) -
(centroid / newScale + pan / zoom)
return Offset(
newOffset.x.coerceIn(0f, (size.width / zoom) * (zoom - 1f)),
newOffset.y.coerceIn(0f, (size.height / zoom) * (zoom - 1f))
)
}
fun calculateDoubleTapOffset(
zoom: Float,
size: IntSize,
tapOffset: Offset
): Offset {
val newOffset = Offset(tapOffset.x, tapOffset.y)
return Offset(
newOffset.x.coerceIn(0f, (size.width / zoom) * (zoom - 1f)),
newOffset.y.coerceIn(0f, (size.height / zoom) * (zoom - 1f))
)
}

View file

@ -6,6 +6,7 @@ import android.app.Dialog
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.View
@ -14,6 +15,9 @@ import android.widget.Button
import android.widget.ImageButton
import android.widget.TextView
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@ -22,6 +26,9 @@ import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import fr.free.nrw.commons.R
import fr.free.nrw.commons.customselector.data.MediaReader
import fr.free.nrw.commons.customselector.database.NotForUploadStatus
@ -31,10 +38,10 @@ import fr.free.nrw.commons.customselector.listeners.FolderClickListener
import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
import fr.free.nrw.commons.customselector.model.Image
import fr.free.nrw.commons.customselector.ui.screens.CustomSelectorScreen
import fr.free.nrw.commons.customselector.ui.screens.ViewImageScreen
import fr.free.nrw.commons.databinding.ActivityCustomSelectorBinding
import fr.free.nrw.commons.databinding.CustomSelectorBottomLayoutBinding
import fr.free.nrw.commons.databinding.CustomSelectorToolbarBinding
import fr.free.nrw.commons.filepicker.Constants
import fr.free.nrw.commons.media.ZoomableActivity
import fr.free.nrw.commons.theme.BaseActivity
import fr.free.nrw.commons.ui.theme.CommonsTheme
@ -129,7 +136,7 @@ class CustomSelectorActivity :
private var showPartialAccessIndicator by mutableStateOf(false)
private val startForResult = registerForActivityResult(StartActivityForResult()){ result ->
private val startForResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()){ result ->
onFullScreenDataReceived(result)
}
@ -137,6 +144,7 @@ class CustomSelectorActivity :
* onCreate Activity, sets theme, initialises the view model, setup view.
*/
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE &&
ContextCompat.checkSelfPermission(
@ -182,13 +190,31 @@ class CustomSelectorActivity :
val uiState by csViewModel.uiState.collectAsStateWithLifecycle()
CommonsTheme {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "main") {
composable(route = "main") {
CustomSelectorScreen(
uiState = uiState,
onEvent = csViewModel::onEvent,
onViewImage = { navController.navigate("view_image/$it") },
selectedImageIds = { uiState.selectedImageIds },
hasPartialAccess = showPartialAccessIndicator
)
}
composable(route = "view_image/{imageId}") { backStackEntry->
val imageId = backStackEntry.arguments?.getString("imageId")?.toLongOrNull()
val imageUri = uiState.filteredImages.find { it.id == imageId }?.uri ?: Uri.EMPTY
val imageIndex = uiState.filteredImages.indexOfFirst { it.id == imageId }
ViewImageScreen(
currentImageIndex = imageIndex,
imageList = uiState.filteredImages
)
}
}
}
}
// setupViews()