mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-27 21:03:54 +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" />
|
android:label="@string/result" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".customselector.ui.selector.CustomSelectorActivity"
|
android:name=".customselector.ui.selector.CustomSelectorActivity"
|
||||||
|
android:windowSoftInputMode="adjustResize"
|
||||||
android:configChanges="screenSize|keyboard|orientation"
|
android:configChanges="screenSize|keyboard|orientation"
|
||||||
android:label="@string/title_activity_custom_selector"
|
android:label="@string/title_activity_custom_selector"
|
||||||
android:parentActivityName=".contributions.MainActivity" />
|
android:parentActivityName=".contributions.MainActivity" />
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ fun CustomSelectorScreen(
|
||||||
uiState: CustomSelectorState,
|
uiState: CustomSelectorState,
|
||||||
onEvent: (CustomSelectorEvent)-> Unit,
|
onEvent: (CustomSelectorEvent)-> Unit,
|
||||||
selectedImageIds: ()-> Set<Long>,
|
selectedImageIds: ()-> Set<Long>,
|
||||||
|
onViewImage: (id: Long)-> Unit,
|
||||||
hasPartialAccess: Boolean = false
|
hasPartialAccess: Boolean = false
|
||||||
) {
|
) {
|
||||||
val adaptiveInfo = currentWindowAdaptiveInfo()
|
val adaptiveInfo = currentWindowAdaptiveInfo()
|
||||||
|
|
@ -96,6 +97,7 @@ fun CustomSelectorScreen(
|
||||||
selectedFolder = folder,
|
selectedFolder = folder,
|
||||||
selectedImages = selectedImageIds,
|
selectedImages = selectedImageIds,
|
||||||
onNavigateBack = { navigator.navigateBack() },
|
onNavigateBack = { navigator.navigateBack() },
|
||||||
|
onViewImage = onViewImage,
|
||||||
onEvent = onEvent,
|
onEvent = onEvent,
|
||||||
adaptiveInfo = adaptiveInfo,
|
adaptiveInfo = adaptiveInfo,
|
||||||
hasPartialAccess = hasPartialAccess
|
hasPartialAccess = hasPartialAccess
|
||||||
|
|
@ -264,6 +266,7 @@ private fun CustomSelectorScreenPreview() {
|
||||||
CommonsTheme {
|
CommonsTheme {
|
||||||
CustomSelectorScreen(
|
CustomSelectorScreen(
|
||||||
uiState = CustomSelectorState(),
|
uiState = CustomSelectorState(),
|
||||||
|
onViewImage = { },
|
||||||
onEvent = { },
|
onEvent = { },
|
||||||
selectedImageIds = { emptySet() },
|
selectedImageIds = { emptySet() },
|
||||||
hasPartialAccess = true
|
hasPartialAccess = true
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ fun ImagesPane(
|
||||||
selectedFolder: Folder,
|
selectedFolder: Folder,
|
||||||
selectedImages: () -> Set<Long>,
|
selectedImages: () -> Set<Long>,
|
||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
|
onViewImage: (id: Long)-> Unit,
|
||||||
onEvent: (CustomSelectorEvent) -> Unit,
|
onEvent: (CustomSelectorEvent) -> Unit,
|
||||||
adaptiveInfo: WindowAdaptiveInfo,
|
adaptiveInfo: WindowAdaptiveInfo,
|
||||||
hasPartialAccess: Boolean = false
|
hasPartialAccess: Boolean = false
|
||||||
|
|
@ -128,7 +129,7 @@ fun ImagesPane(
|
||||||
}
|
}
|
||||||
|
|
||||||
LazyVerticalGrid(
|
LazyVerticalGrid(
|
||||||
columns = GridCells.Adaptive(116.dp),
|
columns = GridCells.Adaptive(96.dp),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.imageGridDragHandler(
|
.imageGridDragHandler(
|
||||||
|
|
@ -159,6 +160,8 @@ fun ImagesPane(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (uiState.inSelectionMode) {
|
if (uiState.inSelectionMode) {
|
||||||
onEvent(CustomSelectorEvent.OnImageSelection(image.id))
|
onEvent(CustomSelectorEvent.OnImageSelection(image.id))
|
||||||
|
} else {
|
||||||
|
onViewImage(image.id)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLongClick = {
|
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.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
|
@ -14,6 +15,9 @@ import android.widget.Button
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.activity.compose.setContent
|
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.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
|
@ -22,6 +26,9 @@ import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
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.R
|
||||||
import fr.free.nrw.commons.customselector.data.MediaReader
|
import fr.free.nrw.commons.customselector.data.MediaReader
|
||||||
import fr.free.nrw.commons.customselector.database.NotForUploadStatus
|
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.listeners.ImageSelectListener
|
||||||
import fr.free.nrw.commons.customselector.model.Image
|
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.CustomSelectorScreen
|
||||||
|
import fr.free.nrw.commons.customselector.ui.screens.ViewImageScreen
|
||||||
import fr.free.nrw.commons.databinding.ActivityCustomSelectorBinding
|
import fr.free.nrw.commons.databinding.ActivityCustomSelectorBinding
|
||||||
import fr.free.nrw.commons.databinding.CustomSelectorBottomLayoutBinding
|
import fr.free.nrw.commons.databinding.CustomSelectorBottomLayoutBinding
|
||||||
import fr.free.nrw.commons.databinding.CustomSelectorToolbarBinding
|
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.media.ZoomableActivity
|
||||||
import fr.free.nrw.commons.theme.BaseActivity
|
import fr.free.nrw.commons.theme.BaseActivity
|
||||||
import fr.free.nrw.commons.ui.theme.CommonsTheme
|
import fr.free.nrw.commons.ui.theme.CommonsTheme
|
||||||
|
|
@ -129,7 +136,7 @@ class CustomSelectorActivity :
|
||||||
|
|
||||||
private var showPartialAccessIndicator by mutableStateOf(false)
|
private var showPartialAccessIndicator by mutableStateOf(false)
|
||||||
|
|
||||||
private val startForResult = registerForActivityResult(StartActivityForResult()){ result ->
|
private val startForResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()){ result ->
|
||||||
onFullScreenDataReceived(result)
|
onFullScreenDataReceived(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -137,6 +144,7 @@ class CustomSelectorActivity :
|
||||||
* onCreate Activity, sets theme, initialises the view model, setup view.
|
* onCreate Activity, sets theme, initialises the view model, setup view.
|
||||||
*/
|
*/
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
enableEdgeToEdge()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE &&
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE &&
|
||||||
ContextCompat.checkSelfPermission(
|
ContextCompat.checkSelfPermission(
|
||||||
|
|
@ -182,12 +190,30 @@ class CustomSelectorActivity :
|
||||||
val uiState by csViewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by csViewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
CommonsTheme {
|
CommonsTheme {
|
||||||
CustomSelectorScreen(
|
val navController = rememberNavController()
|
||||||
uiState = uiState,
|
|
||||||
onEvent = csViewModel::onEvent,
|
NavHost(navController = navController, startDestination = "main") {
|
||||||
selectedImageIds = { uiState.selectedImageIds },
|
composable(route = "main") {
|
||||||
hasPartialAccess = showPartialAccessIndicator
|
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