Skip to content

Commit

Permalink
Fix pinch zoom grid
Browse files Browse the repository at this point in the history
  • Loading branch information
ismartcoding committed May 19, 2024
1 parent 1b9d2b6 commit fb55797
Show file tree
Hide file tree
Showing 13 changed files with 295 additions and 239 deletions.
2 changes: 2 additions & 0 deletions app/src/main/java/com/ismartcoding/plain/Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,11 @@ object AudioPlayModePreference : BasePreference<Int>() {
}
}

object ImageGridCellsIndexPreference : BasePreference<Int>() {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ fun Modifier.gridDragSelect(
state.removeSelected(it)
}
}
val item = items[itemPosition]
this.dragState = dragState.copy(current = itemPosition)
}
},
Expand Down Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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())
}

}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<GridCells>,
initialCellsIndex: Int,
gridState: LazyGridState = rememberLazyGridState(),
animationSpec: AnimationSpec<Float> = defaultAnimationSpec,
): PinchZoomGridState {
val coroutineScope = rememberCoroutineScope()
return remember(coroutineScope, cellsList, initialCellsIndex, gridState) {
Expand All @@ -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<GridCells>,
initialCellsIndex: Int,
internal var animationSpec: AnimationSpec<Float>,
) {
/**
* The current grid cells.
*/
var currentCells by mutableStateOf(cellsList[initialCellsIndex])
private set

val currentCellsIndex: Int
get() = cellsList.indexOf(currentCells)

internal var nextCells by mutableStateOf<GridCells?>(null)

/**
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
}
}
Expand Down Expand Up @@ -352,7 +337,7 @@ private val slowAnimationSpec = spring<Float>(
stiffness = Spring.StiffnessLow,
)

private val defaultAnimationSpec = spring<Float>(
private val PZ_DEFAULT_ANIMATION_SPEC = spring<Float>(
dampingRatio = Spring.DampingRatioLowBouncy + 0.15f,
stiffness = Spring.StiffnessLow + 50f,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 =
Expand Down
Loading

0 comments on commit fb55797

Please sign in to comment.