mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-27 12:53:55 +01:00
add view image screen and enable edge to edge for custom selector
This commit is contained in:
parent
071bffbfa8
commit
a930d8eca6
5 changed files with 221 additions and 9 deletions
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
)
|
||||
}
|
||||
|
|
@ -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,12 +190,30 @@ class CustomSelectorActivity :
|
|||
val uiState by csViewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
CommonsTheme {
|
||||
CustomSelectorScreen(
|
||||
uiState = uiState,
|
||||
onEvent = csViewModel::onEvent,
|
||||
selectedImageIds = { uiState.selectedImageIds },
|
||||
hasPartialAccess = showPartialAccessIndicator
|
||||
)
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue