Add pinch/pan/double-tap zoom to the full-screen photo viewer, mirroring the web viewer semantics: - scale is a multiplier of the natural size, capped at 200%; the fitted view is the lower bound - double tap toggles fitted view <-> 100% natural anchored at the tap - the original loads on zoom, with the preview kept as a placeholder so entering zoom never flashes a black surface - the pager is disabled while zoomed (drags pan the image) and the zoom resets when leaving a photo - video and 360 panoramas are unaffected Implement PhotoZoom (zoom math) and ZoomableImage (gestures), wire it into ViewerContent/MediaPage, and cover it with unit, Compose and device tests. OpenSpec change: add-mobile-photo-zoom
68 lines
2.3 KiB
Kotlin
68 lines
2.3 KiB
Kotlin
package com.galo.android
|
|
|
|
import androidx.compose.foundation.layout.Box
|
|
import androidx.compose.foundation.layout.fillMaxSize
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.geometry.Offset
|
|
import androidx.compose.ui.test.doubleClick
|
|
import androidx.compose.ui.test.junit4.createComposeRule
|
|
import androidx.compose.ui.test.onNodeWithTag
|
|
import androidx.compose.ui.test.performTouchInput
|
|
import androidx.compose.ui.test.pinch
|
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
import com.galo.android.ui.theme.GaloTheme
|
|
import com.galo.android.ui.viewer.TAG_ZOOMABLE_IMAGE
|
|
import com.galo.android.ui.viewer.ZoomableImage
|
|
import org.junit.Assert.assertFalse
|
|
import org.junit.Assert.assertTrue
|
|
import org.junit.Rule
|
|
import org.junit.Test
|
|
import org.junit.runner.RunWith
|
|
import java.util.concurrent.atomic.AtomicBoolean
|
|
|
|
/**
|
|
* Device check for the photo zoom: a real pinch enlarges the image and a double
|
|
* tap returns it to the fitted view (the 200% cap and pan clamp are covered by
|
|
* the unit tests for `PhotoZoom`).
|
|
*/
|
|
@RunWith(AndroidJUnit4::class)
|
|
class ZoomGestureTest {
|
|
|
|
@get:Rule
|
|
val compose = createComposeRule()
|
|
|
|
@Test
|
|
fun pinchZoomsAndDoubleTapResets() {
|
|
val zoomed = AtomicBoolean(false)
|
|
compose.setContent {
|
|
GaloTheme(dynamicColor = false) {
|
|
Box(Modifier.fillMaxSize()) {
|
|
ZoomableImage(
|
|
previewUrl = null,
|
|
hiResUrl = null,
|
|
contentDescription = null,
|
|
naturalWidth = 2000,
|
|
naturalHeight = 1000,
|
|
onZoomChanged = { zoomed.set(it) },
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
compose.onNodeWithTag(TAG_ZOOMABLE_IMAGE).performTouchInput {
|
|
pinch(
|
|
start0 = center + Offset(-60f, 0f),
|
|
end0 = center + Offset(-260f, 0f),
|
|
start1 = center + Offset(60f, 0f),
|
|
end1 = center + Offset(260f, 0f),
|
|
)
|
|
}
|
|
compose.waitForIdle()
|
|
assertTrue("pinch should zoom the image", zoomed.get())
|
|
|
|
compose.onNodeWithTag(TAG_ZOOMABLE_IMAGE).performTouchInput { doubleClick() }
|
|
compose.waitForIdle()
|
|
assertFalse("double tap should return to the fitted view", zoomed.get())
|
|
}
|
|
}
|