From 5b8ee12544c55866e278c5319e80aa87d5afe483 Mon Sep 17 00:00:00 2001 From: Saket Narayan Date: Sat, 13 Jul 2024 15:38:55 -0400 Subject: [PATCH] Prepare to release v0.12.0 --- docs/zoomable/index.md | 42 +++++++++++ docs/zoomable/recipes.md | 22 ++++++ docs/zoomableimage/custom-image-loaders.md | 2 +- docs/zoomableimage/index.md | 72 ++++++++++++++++--- docs/zoomableimage/recipes.md | 1 + gradle.properties | 2 +- mkdocs.yml | 2 +- .../saket/telephoto/zoomable/ZoomableImage.kt | 2 +- .../zoomable/HardwareShortcutsSpec.kt | 5 +- .../DefaultHardwareShortcutDetector.kt | 1 + 10 files changed, 138 insertions(+), 13 deletions(-) diff --git a/docs/zoomable/index.md b/docs/zoomable/index.md index 9175ed10..57b93991 100644 --- a/docs/zoomable/index.md +++ b/docs/zoomable/index.md @@ -8,6 +8,7 @@ A `Modifier` for handling pan & zoom gestures, designed to be shared across all - Haptic feedback for over/under zoom - Compatibility with nested scrolling - Click listeners +- [Keyboard and mouse shortcuts](#keyboard-and-mouse-shortcuts) ### Installation @@ -73,6 +74,15 @@ Modifier.zoomable( ) ``` +The default behavior of toggling between minimum and maximum zoom levels on double-clicks can be overridden by using +the `onDoubleClick` parameter: + +```kotlin +Modifier.zoomable( + onDoubleClick = { state, centroid -> … }, +) +``` + ### Applying gesture transformations When pan & zoom gestures are received, `Modifier.zoomable()` automatically applies their resulting `scale` and `translation` onto your content using `Modifier.graphicsLayer()`. @@ -98,3 +108,35 @@ Text( } ) ``` + +# Keyboard and Mouse shortcuts + +`ZoomableImage()` can observe keyboard and mouse shortcuts for panning and zooming when it is focused, either by the +user or using a `FocusRequester`: + +```kotlin hl_lines="6 11" +val focusRequester = remember { FocusRequester() } +LaunchedEffect(Unit) { + // Automatically request focus when the image is displayed. This assumes there + // is only one zoomable image present in the hierarchy. If you're displaying + // multiple images in a pager, apply this only for the active page. + focusRequester.requestFocus() +} + +Box( + Modifier + .focusRequester(focusRequester) + .zoomable(), +) +``` + +By default, the following shortcuts are recognized. These can be customized (or disabled) by passing a +custom `HardwareShortcutsSpec` to `rememberZoomableState()`. + +| | Android | Desktop | +|-----------|--------------------|-----------------------| +| Zoom in | `Control` + `=` | `Meta` + `=` | +| Zoom out | `Control` + `-` | `Meta` + `-` | +| Pan | Arrow keys | Arrow keys | +| Extra pan | `Alt` + arrow keys | `Option` + arrow keys | + diff --git a/docs/zoomable/recipes.md b/docs/zoomable/recipes.md index b472c31e..ca48f29e 100644 --- a/docs/zoomable/recipes.md +++ b/docs/zoomable/recipes.md @@ -20,6 +20,28 @@ LaunchedEffect(isZoomedOut) { } ``` +### Controlling pan & zoom + +```kotlin +val state = rememberZoomableState() +Box( + Modifier.zoomable(state) +) + +Button(onClick = { state.zoomBy(zoomFactor = 1.2f) }) { + Text("+") +} +Button(onClick = { state.zoomBy(zoomFactor = 1 / 1.2f) }) { + Text("-") +} +Button(onClick = { state.panBy(offset = 50.dp) }) { + Text(">") +} +Button(onClick = { state.panBy(offset = -50.dp) }) { + Text("<") +} +``` + ### Resetting zoom `Modifier.zoomable()` will automatically retain its pan & zoom across state restorations. You may want to prevent this in lazy layouts such as a `Pager()`, where each page is restored every time it becomes visible. diff --git a/docs/zoomableimage/custom-image-loaders.md b/docs/zoomableimage/custom-image-loaders.md index 11ecfc65..83bb7086 100644 --- a/docs/zoomableimage/custom-image-loaders.md +++ b/docs/zoomableimage/custom-image-loaders.md @@ -1,6 +1,6 @@ # Custom image loaders -In its essence, `ZoomableImage` is simply an abstraction over an image loading library. If your preferred library isn't supported by `telephoto` out of the box, you can create your own by extending `ZoomableImageSource`. +In its essence, `ZoomableImage` is simply an abstraction over an image loading library. If your preferred library isn't supported by `telephoto` out of the box, you can create your own by implementing `ZoomableImageSource`. ```kotlin @Composable diff --git a/docs/zoomableimage/index.md b/docs/zoomableimage/index.md index d913fdd6..39bbb212 100644 --- a/docs/zoomableimage/index.md +++ b/docs/zoomableimage/index.md @@ -13,6 +13,7 @@ A _drop-in_ replacement for async `Image()` composables featuring support for pa - Haptic feedback for over/under zoom - Compatibility with nested scrolling - Click listeners +- [Keyboard and mouse shortcuts](#keyboard-and-mouse-shortcuts) ### Installation @@ -52,12 +53,8 @@ For complex scenarios, `ZoomableImage` can also take full image requests: model = ImageRequest.Builder(LocalContext.current) .data("https://example.com/image.jpg") .listener( - remember { - object : ImageRequest.Listener { - override fun onSuccess(…) {} - override fun onError(…) {} - } - } + onSuccess = { … }, + onError = { … }, ) .crossfade(1_000) .memoryCachePolicy(CachePolicy.DISABLED) @@ -178,7 +175,7 @@ Unlike `Image()`, `ZoomableImage` can pan images even when they're cropped. This For detecting double clicks, `ZoomableImage` consumes all tap gestures making it incompatible with `Modifier.clickable()` and `Modifier.combinedClickable()`. As an alternative, its `onClick` and `onLongClick` parameters can be used. === "Coil" - ```kotlin + ```kotlin hl_lines="4-5" ZoomableAsyncImage( modifier = Modifier.clickable { error("This will not work") }, model = "https://example.com/image.jpg", @@ -187,7 +184,7 @@ For detecting double clicks, `ZoomableImage` consumes all tap gestures making it ) ``` === "Glide" - ```kotlin + ```kotlin hl_lines="4-5" ZoomableGlideImage( modifier = Modifier.clickable { error("This will not work") }, model = "https://example.com/image.jpg", @@ -196,6 +193,65 @@ For detecting double clicks, `ZoomableImage` consumes all tap gestures making it ) ``` +The default behavior of toggling between minimum and maximum zoom levels on double-clicks can be overridden by using the `onDoubleClick` parameter: + +=== "Coil" + ```kotlin + ZoomableAsyncImage( + model = "https://example.com/image.jpg", + onDoubleClick = { state, centroid -> … }, + ) + ``` +=== "Glide" + ```kotlin + ZoomableGlideImage( + model = "https://example.com/image.jpg", + onDoubleClick = { state, centroid -> … }, + ) + ``` + +```kotlin +Modifier.zoomable( + onDoubleClick = { state, centroid -> … } +) +``` + +# Keyboard and Mouse shortcuts + +`ZoomableImage()` can observe keyboard and mouse shortcuts for panning and zooming when it is focused, either by the user or using a `FocusRequester`: + +```kotlin hl_lines="6" +val focusRequester = remember { FocusRequester() } +LaunchedEffect(Unit) { + // Automatically request focus when the image is displayed. This assumes there + // is only one zoomable image present in the hierarchy. If you're displaying + // multiple images in a pager, apply this only for the active page. + focusRequester.requestFocus() +} +``` + +=== "Coil" + ```kotlin hl_lines="2" + ZoomableAsyncImage( + modifier = Modifier.focusRequester(focusRequester), + model = "https://example.com/image.jpg", + ) + ``` +=== "Glide" + ```kotlin hl_lines="2" + ZoomableGlideImage( + modifier = Modifier.focusRequester(focusRequester), + model = "https://example.com/image.jpg", + ) + ``` + +By default, the following shortcuts are recognized. These can be customized (or disabled) by passing a custom `HardwareShortcutsSpec` to `rememberZoomableState()`. + +|-----------|--------------------| +| Zoom in | `Control` + `=` | +| Zoom out | `Control` + `-` | +| Pan | Arrow keys | +| Extra pan | `Alt` + arrow keys | ### Sharing hoisted state diff --git a/docs/zoomableimage/recipes.md b/docs/zoomableimage/recipes.md index 9cfa715f..81492777 100644 --- a/docs/zoomableimage/recipes.md +++ b/docs/zoomableimage/recipes.md @@ -2,6 +2,7 @@ ### Modifier.zoomable() - [Observing pan & zoom](../zoomable/recipes.md#observing-pan-zoom) +- [Controlling pan & zoom](../zoomable/recipes.md#controlling-pan-zoom) - [Resetting zoom](../zoomable/recipes.md#resetting-zoom) ### Setting zoom limits diff --git a/gradle.properties b/gradle.properties index 177c969d..0cb872fb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,7 @@ kotlin.mpp.androidSourceSetLayoutVersion=2 android.enableBuildConfigAsBytecode=true GROUP=me.saket.telephoto -VERSION_NAME=0.12.0-SNAPSHOT +VERSION_NAME=0.12.0 SONATYPE_HOST=DEFAULT RELEASE_SIGNING_ENABLED=true diff --git a/mkdocs.yml b/mkdocs.yml index ae8dda89..7d3a9a05 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -65,7 +65,7 @@ plugins: extra: versions: - telephoto: '0.11.2' # Env var for the latest version on maven. + telephoto: '0.12.0' # Env var for the latest version on maven. nav: - 'Overview': index.md diff --git a/zoomable-image/core/src/main/kotlin/me/saket/telephoto/zoomable/ZoomableImage.kt b/zoomable-image/core/src/main/kotlin/me/saket/telephoto/zoomable/ZoomableImage.kt index c4b171bb..19b522ce 100644 --- a/zoomable-image/core/src/main/kotlin/me/saket/telephoto/zoomable/ZoomableImage.kt +++ b/zoomable-image/core/src/main/kotlin/me/saket/telephoto/zoomable/ZoomableImage.kt @@ -38,8 +38,8 @@ import me.saket.telephoto.subsamplingimage.SubSamplingImage import me.saket.telephoto.subsamplingimage.rememberSubSamplingImageState import me.saket.telephoto.zoomable.internal.FocusForwarder import me.saket.telephoto.zoomable.internal.PlaceholderBoundsProvider -import me.saket.telephoto.zoomable.internal.focusable import me.saket.telephoto.zoomable.internal.focusForwarder +import me.saket.telephoto.zoomable.internal.focusable import me.saket.telephoto.zoomable.internal.scaledToMatch /** diff --git a/zoomable/src/commonMain/kotlin/me/saket/telephoto/zoomable/HardwareShortcutsSpec.kt b/zoomable/src/commonMain/kotlin/me/saket/telephoto/zoomable/HardwareShortcutsSpec.kt index 7fbdcf31..b5c69f9f 100644 --- a/zoomable/src/commonMain/kotlin/me/saket/telephoto/zoomable/HardwareShortcutsSpec.kt +++ b/zoomable/src/commonMain/kotlin/me/saket/telephoto/zoomable/HardwareShortcutsSpec.kt @@ -8,12 +8,15 @@ import dev.drewhamilton.poko.Poko * Describes how keyboard and mouse shortcuts are handled. When [enabled], [Modifier.zoomable][zoomable]'s * content will participate in the focus system to receive hardware events. * - * Keep in mind that hardware shortcuts will not work until your zoomable content is focused. + * Keep in mind that hardware shortcuts will only work when your zoomable content is focused. * To do this automatically, use a [FocusRequester]: * * ``` * val focusRequester = remember { FocusRequester() } * LaunchedEffect(Unit) { + * // Automatically request focus when the image is displayed. This assumes there + * // is only one zoomable image present in the hierarchy. If you're displaying + * // multiple images in a pager, apply this only for the active page. * focusRequester.requestFocus() * } * diff --git a/zoomable/src/commonMain/kotlin/me/saket/telephoto/zoomable/internal/DefaultHardwareShortcutDetector.kt b/zoomable/src/commonMain/kotlin/me/saket/telephoto/zoomable/internal/DefaultHardwareShortcutDetector.kt index 0f20dcf3..923a04c9 100644 --- a/zoomable/src/commonMain/kotlin/me/saket/telephoto/zoomable/internal/DefaultHardwareShortcutDetector.kt +++ b/zoomable/src/commonMain/kotlin/me/saket/telephoto/zoomable/internal/DefaultHardwareShortcutDetector.kt @@ -19,6 +19,7 @@ import me.saket.telephoto.zoomable.HardwareShortcutDetector.ShortcutEvent.ZoomDi import kotlin.math.absoluteValue internal object DefaultHardwareShortcutDetector : HardwareShortcutDetector { + override fun detectKey(event: KeyEvent): ShortcutEvent? { // Note for self: Some devices/peripherals have dedicated zoom buttons that map to Key.ZoomIn // and Key.ZoomOut. Examples include: Samsung Galaxy Camera, a motorcycle handlebar controller.