From fb55797c9df82826a3ba4c1a7c90800a74d70fd0 Mon Sep 17 00:00:00 2001 From: iSmartCoding Date: Sun, 19 May 2024 23:13:07 +0800 Subject: [PATCH] Fix pinch zoom grid --- .../java/com/ismartcoding/plain/Constants.kt | 2 + .../plain/preference/Preferences.kt | 5 + .../ui/base/dragselect/GridDragSelect.kt | 3 +- .../ui/base/pinchzoomgrid/GestureModifiers.kt | 141 ++++++++++----- .../base/pinchzoomgrid/PinchZoomGridLayout.kt | 15 +- .../base/pinchzoomgrid/PinchZoomGridState.kt | 37 ++-- .../pullrefresh/LoadMoreRefreshContent.kt | 9 +- .../plain/ui/components/ImageGridItem.kt | 63 ++----- .../ui/components/mediaviewer/MediaViewer.kt | 162 +++++++++--------- .../mediaviewer/previewer/MediaTransform.kt | 4 +- .../plain/ui/page/images/ImageFoldersPage.kt | 2 +- .../plain/ui/page/images/ImagesPage.kt | 81 +++++++-- .../ismartcoding/plain/ui/theme/PlainTheme.kt | 10 +- 13 files changed, 295 insertions(+), 239 deletions(-) diff --git a/app/src/main/java/com/ismartcoding/plain/Constants.kt b/app/src/main/java/com/ismartcoding/plain/Constants.kt index 2bdb7f85..52910b22 100644 --- a/app/src/main/java/com/ismartcoding/plain/Constants.kt +++ b/app/src/main/java/com/ismartcoding/plain/Constants.kt @@ -18,4 +18,6 @@ object Constants { const val ONE_DAY_MS = ONE_DAY * 1000L const val BROADCAST_ACTION_SERVICE = "${BuildConfig.APPLICATION_ID}.action.service" const val BROADCAST_ACTION_ACTIVITY = "${BuildConfig.APPLICATION_ID}.action.activity" + + const val DEFAULT_CELLS_INDEX_WITH_LABEL = 4 } diff --git a/app/src/main/java/com/ismartcoding/plain/preference/Preferences.kt b/app/src/main/java/com/ismartcoding/plain/preference/Preferences.kt index 685b2290..0ac94956 100644 --- a/app/src/main/java/com/ismartcoding/plain/preference/Preferences.kt +++ b/app/src/main/java/com/ismartcoding/plain/preference/Preferences.kt @@ -389,6 +389,11 @@ object AudioPlayModePreference : BasePreference() { } } +object ImageGridCellsIndexPreference : BasePreference() { + override val default = 6 + override val key = intPreferencesKey("image_grid_cells_index") +} + abstract class BaseSortByPreference( val prefix: String, private val defaultSort: FileSortBy = FileSortBy.DATE_DESC diff --git a/app/src/main/java/com/ismartcoding/plain/ui/base/dragselect/GridDragSelect.kt b/app/src/main/java/com/ismartcoding/plain/ui/base/dragselect/GridDragSelect.kt index 23b2f359..4313941e 100644 --- a/app/src/main/java/com/ismartcoding/plain/ui/base/dragselect/GridDragSelect.kt +++ b/app/src/main/java/com/ismartcoding/plain/ui/base/dragselect/GridDragSelect.kt @@ -78,7 +78,6 @@ fun Modifier.gridDragSelect( state.removeSelected(it) } } - val item = items[itemPosition] this.dragState = dragState.copy(current = itemPosition) } }, @@ -108,7 +107,7 @@ private fun LazyGridState.itemIndexAtPosition(hitPoint: Offset): Int? { return found?.index } -private fun LazyGridState.getItemPosition(hitPoint: Offset): Int? { +fun LazyGridState.getItemPosition(hitPoint: Offset): Int? { return itemIndexAtPosition(hitPoint) ?: if (isPastLastItem(hitPoint)) layoutInfo.totalItemsCount - 1 else null } diff --git a/app/src/main/java/com/ismartcoding/plain/ui/base/pinchzoomgrid/GestureModifiers.kt b/app/src/main/java/com/ismartcoding/plain/ui/base/pinchzoomgrid/GestureModifiers.kt index cdc8eb31..2c063557 100644 --- a/app/src/main/java/com/ismartcoding/plain/ui/base/pinchzoomgrid/GestureModifiers.kt +++ b/app/src/main/java/com/ismartcoding/plain/ui/base/pinchzoomgrid/GestureModifiers.kt @@ -1,82 +1,129 @@ package com.ismartcoding.plain.ui.base.pinchzoomgrid +import android.content.Context import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.gestures.awaitFirstDown import androidx.compose.foundation.gestures.calculateCentroid import androidx.compose.foundation.gestures.calculateCentroidSize import androidx.compose.foundation.gestures.calculateZoom import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.input.pointer.PointerEvent +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.PointerInputScope import androidx.compose.ui.input.pointer.positionChanged import androidx.compose.ui.input.pointer.util.VelocityTracker import androidx.compose.ui.input.pointer.util.addPointerInputChange import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastFirstOrNull import androidx.compose.ui.util.fastForEach +import com.ismartcoding.lib.helpers.CoroutinesHelper.coIO +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import kotlin.math.abs -internal fun Modifier.handlePinchGesture(state: PinchZoomGridState): Modifier { - return this.pointerInput(state) { - // Based on detectTransformGestures() - val velocityTracker = VelocityTracker() - awaitEachGesture { - var zoom = 1f - var pastTouchSlop = false - val touchSlop = viewConfiguration.touchSlop / 4 +suspend fun PointerInputScope.handlePinchGesture( + context: Context, + state: PinchZoomGridState, + scope: CoroutineScope, + onTap: (Offset) -> Unit = {}, + onLongPress: (Offset) -> Unit = {} +) { + val velocityTracker = VelocityTracker() + val longPressTimeout = viewConfiguration.longPressTimeoutMillis + awaitEachGesture { + var zoom = 1f + var pastTouchSlop = false + val touchSlop = viewConfiguration.touchSlop / 4 - val down = awaitFirstDown(requireUnconsumed = false) + val down = awaitFirstDown(requireUnconsumed = false) + val downTime = System.currentTimeMillis() + var releasedEvent: PointerEvent? = null + var moveCount = 0 - velocityTracker.resetTracking() - velocityTracker.addPointerInputChange(down) - var trackId = down.id + velocityTracker.resetTracking() + velocityTracker.addPointerInputChange(down) + var trackId = down.id - var pinchStarted = false - - do { - val event = awaitPointerEvent() - val canceled = event.changes.fastAny { it.isConsumed } - - val trackChange = event.changes.fastFirstOrNull { it.id == trackId } - ?: event.changes.firstOrNull()?.also { trackId = it.id } - if (trackChange != null) { - velocityTracker.addPointerInputChange(trackChange) + var pinchStarted = false + val longPressJob = coIO { + delay(longPressTimeout) + if (!pastTouchSlop) { + scope.launch { + onLongPress(down.position) } + } + } + + do { + val event = awaitPointerEvent() + if (event.type == PointerEventType.Release) { + releasedEvent = event + } + if (event.type == PointerEventType.Move) { + moveCount++ + } - if (!canceled) { - val zoomChange = event.calculateZoom() + val trackChange = event.changes.fastFirstOrNull { it.id == trackId } + ?: event.changes.firstOrNull()?.also { trackId = it.id } + if (trackChange != null) { + velocityTracker.addPointerInputChange(trackChange) + } - if (!pastTouchSlop) { - zoom *= zoomChange - val centroidSize = event.calculateCentroidSize(useCurrent = false) - val zoomMotion = abs(1 - zoom) * centroidSize + val canceled = event.changes.fastAny { it.isConsumed } + if (!canceled) { + val zoomChange = event.calculateZoom() + if (!pastTouchSlop) { + zoom *= zoomChange + val centroidSize = event.calculateCentroidSize(useCurrent = false) + val zoomMotion = abs(1 - zoom) * centroidSize - if (zoomMotion > touchSlop) { - pastTouchSlop = true - } + if (zoomMotion > touchSlop) { + pastTouchSlop = true + longPressJob.cancel() } + } - if (pastTouchSlop) { - if (zoomChange != 1f) { - if (!pinchStarted) { - val centroid = event.calculateCentroid(useCurrent = false) - state.onZoomStart(centroid, zoom) - pinchStarted = true - } - state.onZoom(zoomChange) + if (pastTouchSlop) { + if (zoomChange != 1f) { + if (!pinchStarted) { + val centroid = event.calculateCentroid(useCurrent = false) + state.onZoomStart(centroid, zoom) + pinchStarted = true } - event.changes.fastForEach { - if (it.positionChanged()) { - it.consume() - } + state.onZoom(zoomChange) + } + event.changes.fastForEach { + if (it.positionChanged()) { + it.consume() } } } - } while (!canceled && event.changes.fastAny { it.pressed }) - if (pinchStarted) { - state.onZoomStopped(velocityTracker.calculateVelocity()) + } + } while (!canceled && event.changes.fastAny { it.pressed }) + + if (moveCount == 0) { + releasedEvent?.let { e -> + if (e.changes.isEmpty()) { + return@let + } + val dt = System.currentTimeMillis() - downTime + if (dt < 300) { + scope.launch { + onTap(e.changes.first().position) + } + } } } + + longPressJob.cancel() + + if (pinchStarted) { + state.onZoomStopped(velocityTracker.calculateVelocity()) + } + } } diff --git a/app/src/main/java/com/ismartcoding/plain/ui/base/pinchzoomgrid/PinchZoomGridLayout.kt b/app/src/main/java/com/ismartcoding/plain/ui/base/pinchzoomgrid/PinchZoomGridLayout.kt index ad46a370..b7352b92 100644 --- a/app/src/main/java/com/ismartcoding/plain/ui/base/pinchzoomgrid/PinchZoomGridLayout.kt +++ b/app/src/main/java/com/ismartcoding/plain/ui/base/pinchzoomgrid/PinchZoomGridLayout.kt @@ -1,20 +1,25 @@ package com.ismartcoding.plain.ui.base.pinchzoomgrid +import android.content.Context import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.lazy.grid.LazyGridScope -import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.pointerInput +import kotlinx.coroutines.CoroutineScope @Composable fun PinchZoomGridLayout( + context: Context, state: PinchZoomGridState, + scope: CoroutineScope, modifier: Modifier = Modifier, + onTap: (Offset) -> Unit = {}, + onLongPress: (Offset) -> Unit = {}, content: @Composable PinchZoomGridScope.() -> Unit, ) { val contentScope = remember(state, state.gridState) { @@ -29,7 +34,9 @@ fun PinchZoomGridLayout( Box( modifier = modifier - .handlePinchGesture(state) + .pointerInput(Unit) { + handlePinchGesture(context, state, scope, onTap, onLongPress) + } .handleOverZooming(state), ) { val nextCells = state.nextCells diff --git a/app/src/main/java/com/ismartcoding/plain/ui/base/pinchzoomgrid/PinchZoomGridState.kt b/app/src/main/java/com/ismartcoding/plain/ui/base/pinchzoomgrid/PinchZoomGridState.kt index 97c03178..6ed5a473 100644 --- a/app/src/main/java/com/ismartcoding/plain/ui/base/pinchzoomgrid/PinchZoomGridState.kt +++ b/app/src/main/java/com/ismartcoding/plain/ui/base/pinchzoomgrid/PinchZoomGridState.kt @@ -1,6 +1,5 @@ package com.ismartcoding.plain.ui.base.pinchzoomgrid -import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.Spring import androidx.compose.animation.core.animate import androidx.compose.animation.core.spring @@ -31,15 +30,11 @@ import kotlinx.coroutines.launch import kotlin.math.abs import kotlin.math.max -/** - * Create and remember a [PinchZoomGridState]. - */ @Composable fun rememberPinchZoomGridState( cellsList: List, initialCellsIndex: Int, gridState: LazyGridState = rememberLazyGridState(), - animationSpec: AnimationSpec = defaultAnimationSpec, ): PinchZoomGridState { val coroutineScope = rememberCoroutineScope() return remember(coroutineScope, cellsList, initialCellsIndex, gridState) { @@ -48,30 +43,23 @@ fun rememberPinchZoomGridState( gridState, cellsList, initialCellsIndex, - animationSpec, ) - }.also { - it.animationSpec = animationSpec } } -/** - * The state used by [PinchZoomGridLayout]. - */ @Stable class PinchZoomGridState( private val coroutineScope: CoroutineScope, internal val gridState: LazyGridState, private val cellsList: List, initialCellsIndex: Int, - internal var animationSpec: AnimationSpec, ) { - /** - * The current grid cells. - */ var currentCells by mutableStateOf(cellsList[initialCellsIndex]) private set + val currentCellsIndex: Int + get() = cellsList.indexOf(currentCells) + internal var nextCells by mutableStateOf(null) /** @@ -149,17 +137,14 @@ class PinchZoomGridState( val job = coroutineContext.job animationJob = job val maxVelocity = max(abs(velocity.x), abs(velocity.y)) - val animationSpec = if (animationSpec == defaultAnimationSpec) { - if (maxVelocity > 1500f) { - fastAnimationSpec - } else if (maxVelocity < 500f) { - slowAnimationSpec - } else { - defaultAnimationSpec - } + val animationSpec = if (maxVelocity > 1500f) { + fastAnimationSpec + } else if (maxVelocity < 500f) { + slowAnimationSpec } else { - animationSpec + PZ_DEFAULT_ANIMATION_SPEC } + if (progress > 0.5f && next != null) { job.invokeOnCompletion { onZoomAnimationEnd(next) } val targetValue = if (zoomType == ZoomType.ZoomIn) { @@ -211,7 +196,7 @@ class PinchZoomGridState( } // Start animation after the new layout is ready awaitFrame() - animate(zoom, targetZoom, animationSpec = animationSpec) { value, _ -> + animate(zoom, targetZoom, animationSpec = PZ_DEFAULT_ANIMATION_SPEC) { value, _ -> zoom = value } } @@ -352,7 +337,7 @@ private val slowAnimationSpec = spring( stiffness = Spring.StiffnessLow, ) -private val defaultAnimationSpec = spring( +private val PZ_DEFAULT_ANIMATION_SPEC = spring( dampingRatio = Spring.DampingRatioLowBouncy + 0.15f, stiffness = Spring.StiffnessLow + 50f, ) diff --git a/app/src/main/java/com/ismartcoding/plain/ui/base/pullrefresh/LoadMoreRefreshContent.kt b/app/src/main/java/com/ismartcoding/plain/ui/base/pullrefresh/LoadMoreRefreshContent.kt index 4b7bedb6..29ad5880 100644 --- a/app/src/main/java/com/ismartcoding/plain/ui/base/pullrefresh/LoadMoreRefreshContent.kt +++ b/app/src/main/java/com/ismartcoding/plain/ui/base/pullrefresh/LoadMoreRefreshContent.kt @@ -1,6 +1,7 @@ package com.ismartcoding.plain.ui.base.pullrefresh import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -13,16 +14,16 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.ismartcoding.plain.R +import com.ismartcoding.plain.ui.base.BottomSpace @Composable fun LoadMoreRefreshContent(isLoadFinish: Boolean = false) { - Row( + Column( modifier = Modifier .fillMaxWidth() - .padding(top = 8.dp, bottom = 16.dp), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, + .padding(top = 8.dp, bottom = 32.dp), + horizontalAlignment = Alignment.CenterHorizontally, ) { Text( text = diff --git a/app/src/main/java/com/ismartcoding/plain/ui/components/ImageGridItem.kt b/app/src/main/java/com/ismartcoding/plain/ui/components/ImageGridItem.kt index 605adf25..0f135ce2 100644 --- a/app/src/main/java/com/ismartcoding/plain/ui/components/ImageGridItem.kt +++ b/app/src/main/java/com/ismartcoding/plain/ui/components/ImageGridItem.kt @@ -2,7 +2,6 @@ package com.ismartcoding.plain.ui.components import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.aspectRatio @@ -23,21 +22,17 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.navigation.NavHostController -import com.ismartcoding.lib.helpers.CoroutinesHelper.coMain -import com.ismartcoding.lib.helpers.CoroutinesHelper.withIO import com.ismartcoding.plain.data.DImage import com.ismartcoding.plain.helpers.FormatHelper import com.ismartcoding.plain.ui.base.dragselect.DragSelectState import com.ismartcoding.plain.ui.components.mediaviewer.previewer.MediaPreviewerState import com.ismartcoding.plain.ui.components.mediaviewer.previewer.TransformImageView +import com.ismartcoding.plain.ui.components.mediaviewer.previewer.TransformItemState import com.ismartcoding.plain.ui.components.mediaviewer.previewer.rememberTransformItemState import com.ismartcoding.plain.ui.models.CastViewModel import com.ismartcoding.plain.ui.models.ImagesViewModel -import com.ismartcoding.plain.ui.models.MediaPreviewData import com.ismartcoding.plain.ui.theme.darkMask import com.ismartcoding.plain.ui.theme.lightMask @@ -45,43 +40,21 @@ import com.ismartcoding.plain.ui.theme.lightMask @Composable fun ImageGridItem( modifier: Modifier = Modifier, - navController: NavHostController, viewModel: ImagesViewModel, castViewModel: CastViewModel, m: DImage, + showSize: Boolean, previewerState: MediaPreviewerState, - dragSelectState: DragSelectState + dragSelectState: DragSelectState, + transformItemStateMap: MutableMap, ) { val isSelected by remember { derivedStateOf { dragSelectState.isSelected(m.id) } } val inSelectionMode = dragSelectState.selectMode val selected = isSelected || viewModel.selectedItem.value?.id == m.id - val context = LocalContext.current val itemState = rememberTransformItemState() + transformItemStateMap[m.id] = itemState Box( modifier = modifier - .combinedClickable( - onClick = { - if (castViewModel.castMode.value) { - castViewModel.cast(m.path) - } else if (inSelectionMode) { - dragSelectState.addSelected(m.id) - } else { - coMain { - withIO { MediaPreviewData.setDataAsync(context, itemState, viewModel.itemsFlow.value, m) } - previewerState.openTransform( - index = MediaPreviewData.items.indexOfFirst { it.id == m.id }, - itemState = itemState, - ) - } - } - }, - onLongClick = { - if (inSelectionMode) { - return@combinedClickable - } - viewModel.selectedItem.value = m - }, - ) .then( if (!inSelectionMode) { Modifier @@ -149,20 +122,22 @@ fun ImageGridItem( dragSelectState.select(m.id) }) } - Box( - modifier = - Modifier - .align(Alignment.BottomEnd) - .background(MaterialTheme.colorScheme.darkMask()), - ) { - Text( + if (showSize) { + Box( modifier = Modifier - .padding(horizontal = 4.dp, vertical = 2.dp), - text = FormatHelper.formatBytes(m.size), - color = Color.White, - style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.Normal), - ) + .align(Alignment.BottomEnd) + .background(MaterialTheme.colorScheme.darkMask()), + ) { + Text( + modifier = + Modifier + .padding(horizontal = 4.dp, vertical = 2.dp), + text = FormatHelper.formatBytes(m.size), + color = Color.White, + style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.Normal), + ) + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/MediaViewer.kt b/app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/MediaViewer.kt index 31377b41..edf33299 100644 --- a/app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/MediaViewer.kt +++ b/app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/MediaViewer.kt @@ -6,6 +6,7 @@ import androidx.compose.animation.core.FloatExponentialDecaySpec import androidx.compose.animation.core.SpringSpec import androidx.compose.animation.core.generateDecayAnimationSpec import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.gestures.awaitFirstDown import androidx.compose.foundation.gestures.calculateCentroid import androidx.compose.foundation.gestures.calculateCentroidSize @@ -500,7 +501,7 @@ fun MediaViewer( state.scale.snapTo(desScale) state.offsetX.snapTo(desX) state.offsetY.snapTo(desY) - // state.rotation.snapTo(desRotation) + // state.rotation.snapTo(desRotation) } // 这里判断是否已运动到边界,如果到了边界,就不消费事件,让上层界面获取到事件 @@ -597,93 +598,90 @@ suspend fun PointerInputScope.detectTransformGestures( ) { var lastReleaseTime = 0L var scope: CoroutineScope? = null - 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) - val t0 = System.currentTimeMillis() - var releasedEvent: PointerEvent? = null - var moveCount = 0 - // 这里开始事件 - gestureStart() - do { - val event = awaitPointerEvent() - if (event.type == PointerEventType.Release) releasedEvent = event - if (event.type == PointerEventType.Move) moveCount++ - 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 - ) { - if (!onGesture( - centroid, - panChange, - zoomChange, - effectiveRotation, - event - ) - ) break - } + awaitEachGesture { + var rotation = 0f + var zoom = 1f + var pan = Offset.Zero + var pastTouchSlop = false + val touchSlop = viewConfiguration.touchSlop + var lockedToPanZoom = false + + awaitFirstDown(requireUnconsumed = false) + val t0 = System.currentTimeMillis() + var releasedEvent: PointerEvent? = null + var moveCount = 0 + + gestureStart() + do { + val event = awaitPointerEvent() + if (event.type == PointerEventType.Release) releasedEvent = event + if (event.type == PointerEventType.Move) moveCount++ + 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 } } - } while (!canceled && event.changes.fastAny { it.pressed }) - - var t1 = System.currentTimeMillis() - val dt = t1 - t0 - val dlt = t1 - lastReleaseTime - - if (moveCount == 0) releasedEvent?.let { e -> - if (e.changes.isEmpty()) return@let - val offset = e.changes.first().position - if (dlt < 272) { - t1 = 0L - scope?.cancel() - onDoubleTap(offset) - } else if (dt < 200) { - scope = MainScope() - scope?.launch(Dispatchers.Main) { - delay(272) - onTap(offset) + if (pastTouchSlop) { + val centroid = event.calculateCentroid(useCurrent = false) + val effectiveRotation = if (lockedToPanZoom) 0f else rotationChange + if (effectiveRotation != 0f || + zoomChange != 1f || + panChange != Offset.Zero + ) { + if (!onGesture( + centroid, + panChange, + zoomChange, + effectiveRotation, + event + ) + ) break } } - lastReleaseTime = t1 } - - // 这里是事件结束 - gestureEnd(moveCount != 0) + } while (!canceled && event.changes.fastAny { it.pressed }) + + var t1 = System.currentTimeMillis() + val dt = t1 - t0 + val dlt = t1 - lastReleaseTime + + if (moveCount == 0) releasedEvent?.let { e -> + if (e.changes.isEmpty()) return@let + val offset = e.changes.first().position + if (dlt < 272) { + t1 = 0L + scope?.cancel() + onDoubleTap(offset) + } else if (dt < 200) { + scope = MainScope() + scope?.launch(Dispatchers.Main) { + delay(272) + onTap(offset) + } + } + lastReleaseTime = t1 } + + gestureEnd(moveCount != 0) } } diff --git a/app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/previewer/MediaTransform.kt b/app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/previewer/MediaTransform.kt index 021e2efd..9abac4d6 100644 --- a/app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/previewer/MediaTransform.kt +++ b/app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/previewer/MediaTransform.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.composed import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.ContentScale @@ -70,6 +71,7 @@ fun TransformImageView( modifier = if (path.endsWith(".svg", true)) imageModifier.background(Color.White) else imageModifier, model = path, contentDescription = path, + filterQuality = FilterQuality.None, contentScale = ContentScale.Crop, ) } @@ -210,7 +212,7 @@ class TransformContentState( var onAction by mutableStateOf(false) - var onActionTarget by mutableStateOf(null) + private var onActionTarget by mutableStateOf(null) var displayWidth = Animatable(0F) diff --git a/app/src/main/java/com/ismartcoding/plain/ui/page/images/ImageFoldersPage.kt b/app/src/main/java/com/ismartcoding/plain/ui/page/images/ImageFoldersPage.kt index 05bd8e79..31bd4853 100644 --- a/app/src/main/java/com/ismartcoding/plain/ui/page/images/ImageFoldersPage.kt +++ b/app/src/main/java/com/ismartcoding/plain/ui/page/images/ImageFoldersPage.kt @@ -94,7 +94,7 @@ fun ImageFoldersPage( exit = fadeOut() ) { if (itemsState.isNotEmpty()) { - PinchZoomGridLayout(state = pinchState) { + PinchZoomGridLayout(context = context, state = pinchState, scope = scope) { LazyVerticalGrid( state = gridState, modifier = Modifier.fillMaxSize(), diff --git a/app/src/main/java/com/ismartcoding/plain/ui/page/images/ImagesPage.kt b/app/src/main/java/com/ismartcoding/plain/ui/page/images/ImagesPage.kt index 3b30090f..6bf75425 100644 --- a/app/src/main/java/com/ismartcoding/plain/ui/page/images/ImagesPage.kt +++ b/app/src/main/java/com/ismartcoding/plain/ui/page/images/ImagesPage.kt @@ -33,7 +33,6 @@ import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -47,12 +46,14 @@ import androidx.navigation.NavHostController import com.ismartcoding.lib.channel.receiveEventHandler import com.ismartcoding.lib.extensions.isGestureInteractionMode import com.ismartcoding.lib.helpers.CoroutinesHelper.withIO +import com.ismartcoding.plain.Constants import com.ismartcoding.plain.R import com.ismartcoding.plain.data.DMediaBucket import com.ismartcoding.plain.enums.AppFeatureType import com.ismartcoding.plain.features.PermissionsResultEvent import com.ismartcoding.plain.features.locale.LocaleHelper import com.ismartcoding.plain.features.media.CastPlayer +import com.ismartcoding.plain.preference.ImageGridCellsIndexPreference import com.ismartcoding.plain.preference.ImageSortByPreference import com.ismartcoding.plain.ui.base.ActionButtonMoreWithMenu import com.ismartcoding.plain.ui.base.ActionButtonSearch @@ -72,12 +73,10 @@ import com.ismartcoding.plain.ui.base.PMiniOutlineButton import com.ismartcoding.plain.ui.base.PScaffold import com.ismartcoding.plain.ui.base.PTopAppBar import com.ismartcoding.plain.ui.base.dragselect.DragSelectState +import com.ismartcoding.plain.ui.base.dragselect.getItemPosition import com.ismartcoding.plain.ui.base.dragselect.gridDragSelect import com.ismartcoding.plain.ui.base.dragselect.rememberDragSelectState import com.ismartcoding.plain.ui.base.fastscroll.LazyVerticalGridScrollbar -import com.ismartcoding.plain.ui.base.fastscroll.ScrollbarSettings -import com.ismartcoding.plain.ui.components.mediaviewer.previewer.ImagePreviewer -import com.ismartcoding.plain.ui.components.mediaviewer.previewer.rememberPreviewerState import com.ismartcoding.plain.ui.base.pinchzoomgrid.PinchZoomGridLayout import com.ismartcoding.plain.ui.base.pinchzoomgrid.rememberPinchZoomGridState import com.ismartcoding.plain.ui.base.pullrefresh.LoadMoreRefreshContent @@ -88,11 +87,15 @@ import com.ismartcoding.plain.ui.components.CastDialog import com.ismartcoding.plain.ui.components.FileSortDialog import com.ismartcoding.plain.ui.components.ImageGridItem import com.ismartcoding.plain.ui.components.ListSearchBar +import com.ismartcoding.plain.ui.components.mediaviewer.previewer.ImagePreviewer +import com.ismartcoding.plain.ui.components.mediaviewer.previewer.TransformItemState +import com.ismartcoding.plain.ui.components.mediaviewer.previewer.rememberPreviewerState import com.ismartcoding.plain.ui.extensions.navigate import com.ismartcoding.plain.ui.extensions.navigateTags import com.ismartcoding.plain.ui.models.CastViewModel import com.ismartcoding.plain.ui.models.ImageFoldersViewModel import com.ismartcoding.plain.ui.models.ImagesViewModel +import com.ismartcoding.plain.ui.models.MediaPreviewData import com.ismartcoding.plain.ui.models.TagsViewModel import com.ismartcoding.plain.ui.models.enterSearchMode import com.ismartcoding.plain.ui.models.exitSearchMode @@ -122,6 +125,7 @@ fun ImagesPage( bucketsState.associateBy { it.id } } } + val previewerState = rememberPreviewerState() val tagsState by tagsViewModel.itemsFlow.collectAsState() val tagsMapState by tagsViewModel.tagsMapFlow.collectAsState() @@ -130,19 +134,15 @@ fun ImagesPage( var hasPermission by remember { mutableStateOf(AppFeatureType.FILES.hasPermission(context)) } - var lastCellIndex by remember { mutableIntStateOf(4) } - var canScroll by rememberSaveable { mutableStateOf(true) } + val transformItemStateMap = remember { mutableMapOf() } + var initialCellsIndex by remember { mutableIntStateOf(ImageGridCellsIndexPreference.default) } val pinchState = rememberPinchZoomGridState( cellsList = PlainTheme.cellsList, - initialCellsIndex = lastCellIndex + initialCellsIndex = initialCellsIndex ) val dragSelectState = rememberDragSelectState(lazyGridState = pinchState.gridState) - - LaunchedEffect(pinchState.isZooming) { - canScroll = !pinchState.isZooming - lastCellIndex = PlainTheme.cellsList.indexOf(pinchState.currentCells) - } + val canScroll by remember { derivedStateOf { !pinchState.isZooming } } val events by remember { mutableStateOf>(arrayListOf()) } @@ -162,6 +162,7 @@ fun ImagesPage( tagsViewModel.dataType.value = viewModel.dataType if (hasPermission) { scope.launch(Dispatchers.IO) { + initialCellsIndex = ImageGridCellsIndexPreference.getAsync(context) viewModel.sortBy.value = ImageSortByPreference.getValueAsync(context) viewModel.loadAsync(context, tagsViewModel) bucketViewModel.loadAsync(context) @@ -177,6 +178,12 @@ fun ImagesPage( }) } + LaunchedEffect(pinchState.currentCells) { + scope.launch(Dispatchers.IO) { + ImageGridCellsIndexPreference.putAsync(context, pinchState.currentCellsIndex) + } + } + val insetsController = WindowCompat.getInsetsController(window, view) LaunchedEffect(dragSelectState.selectMode, (previewerState.visible && !context.isGestureInteractionMode())) { if (dragSelectState.selectMode || (previewerState.visible && !context.isGestureInteractionMode())) { @@ -202,7 +209,7 @@ fun ImagesPage( } } - BackHandler(enabled = dragSelectState.selectMode || castViewModel.castMode.value || viewModel.showSearchBar.value || previewerState.visible) { + BackHandler { if (previewerState.visible) { scope.launch { previewerState.closeTransform() @@ -216,6 +223,8 @@ fun ImagesPage( viewModel.exitSearchMode() onSearch("") } + } else { + navController.popBackStack() } } @@ -385,11 +394,45 @@ fun ImagesPage( exit = fadeOut() ) { if (itemsState.isNotEmpty()) { - PinchZoomGridLayout(state = pinchState) { + PinchZoomGridLayout(context = context, + state = pinchState, + scope = scope, + + onTap = { offset -> + val itemPosition = pinchState.gridState.getItemPosition(offset) ?: return@PinchZoomGridLayout + val m = itemsState.getOrNull(itemPosition) + if (m != null) { + if (castViewModel.castMode.value) { + castViewModel.cast(m.path) + } else if (dragSelectState.selectMode) { + dragSelectState.addSelected(m.id) + } else { + scope.launch { + val itemState = transformItemStateMap[m.id] ?: return@launch + withIO { MediaPreviewData.setDataAsync(context, itemState, viewModel.itemsFlow.value, m) } + previewerState.openTransform( + index = MediaPreviewData.items.indexOfFirst { it.id == m.id }, + itemState = itemState, + ) + } + } + } + }, + onLongPress = { offset -> + val itemPosition = pinchState.gridState.getItemPosition(offset) ?: return@PinchZoomGridLayout + val m = itemsState.getOrNull(itemPosition) + if (m != null) { + if (dragSelectState.selectMode) { + return@PinchZoomGridLayout + } + viewModel.selectedItem.value = m + } + }) { LazyVerticalGridScrollbar( state = gridState, ) { LazyVerticalGrid( + columns = gridCells, state = gridState, modifier = Modifier .fillMaxSize() @@ -397,10 +440,9 @@ fun ImagesPage( items = itemsState, state = dragSelectState, ), - columns = gridCells, userScrollEnabled = canScroll, - horizontalArrangement = Arrangement.spacedBy(1.dp), - verticalArrangement = Arrangement.spacedBy(1.dp), + horizontalArrangement = Arrangement.spacedBy(2.dp), + verticalArrangement = Arrangement.spacedBy(2.dp), ) { items(itemsState, key = { @@ -416,12 +458,13 @@ fun ImagesPage( modifier = Modifier .pinchItem(key = m.id) .animateItemPlacement(), - navController, viewModel, castViewModel, m, + showSize = pinchState.currentCellsIndex >= Constants.DEFAULT_CELLS_INDEX_WITH_LABEL, previewerState, - dragSelectState + dragSelectState, + transformItemStateMap ) } item( diff --git a/app/src/main/java/com/ismartcoding/plain/ui/theme/PlainTheme.kt b/app/src/main/java/com/ismartcoding/plain/ui/theme/PlainTheme.kt index fc0a62b7..7bd1ce6a 100644 --- a/app/src/main/java/com/ismartcoding/plain/ui/theme/PlainTheme.kt +++ b/app/src/main/java/com/ismartcoding/plain/ui/theme/PlainTheme.kt @@ -22,15 +22,7 @@ object PlainTheme { val APP_BAR_HEIGHT = 64.dp const val ANIMATION_DURATION = 300 - val cellsList = listOf( - GridCells.Fixed(7), - GridCells.Fixed(6), - GridCells.Fixed(5), - GridCells.Fixed(4), - GridCells.Fixed(3), - GridCells.Fixed(2), - GridCells.Fixed(1), - ) + val cellsList = IntRange(2, 10).map { GridCells.Fixed(it) }.reversed() @Composable fun getCardModifier(index: Int = 0, size: Int = 1, selected: Boolean = false): Modifier {