Zoomable Image¶
A drop-in replacement for async Image()
composables featuring support for pan & zoom gestures and automatic sub-sampling of large images. This ensures that images maintain their intricate details even when fully zoomed in, without causing any OutOfMemory
exceptions.
Features
- Automatic sub-sampling of bitmaps
- Gestures:
- Pinch-to-zoom and flings
- Double click to zoom
- Single finger zoom (double-tap and hold)
- Haptic feedback when reaching zoom limits
- Compatibility with nested scrolling
- Click listeners
- Keyboard and mouse shortcuts
- State preservation across config changes (including screen rotations)
Installation¶
Image requests¶
For complex scenarios, ZoomableImage
can also take full image requests:
ZoomableAsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data("https://example.com/image.jpg")
.listener(
onSuccess = { … },
onError = { … },
)
.crossfade(1_000)
.memoryCachePolicy(CachePolicy.DISABLED)
.build(),
imageLoader = LocalContext.current.imageLoader, // Optional.
contentDescription = …
)
ZoomableGlideImage(
model = "https://example.com/image.jpg",
contentDescription = …
) {
it.addListener(object : RequestListener<Drawable> {
override fun onResourceReady(…): Boolean = TODO()
override fun onLoadFailed(…): Boolean = TODO()
})
.transition(withCrossFade(1_000))
.skipMemoryCache(true)
.disallowHardwareConfig()
.timeout(30_000),
}
Placeholders¶
If your images are available in multiple resolutions, telephoto
highly recommends using their lower resolutions as placeholders while their full quality equivalents are loaded in the background.
When combined with a cross-fade transition, ZoomableImage
will smoothly swap out placeholders when their full quality versions are ready to be displayed.
ZoomableAsyncImage(
modifier = Modifier.fillMaxSize(),
model = ImageRequest.Builder(LocalContext.current)
.data("https://example.com/image.jpg")
.placeholderMemoryCacheKey(…)
.crossfade(1_000)
.build(),
contentDescription = …
)
placeholderMemoryCacheKey()
can be found on Coil's website.
ZoomableGlideImage(
modifier = Modifier.fillMaxSize(),
model = "https://example.com/image.jpg",
contentDescription = …
) {
it.thumbnail(…) // or placeholder()
.transition(withCrossFade(1_000)),
}
thumbnail()
can be found on Glide's website.
Warning
Placeholders are visually incompatible with Modifier.wrapContentSize()
.
Content alignment¶
Alignment.TopCenter |
Alignment.BottomCenter |
When images are zoomed, they're scaled with respect to their alignment
until they're large enough to fill all available space. After that, they're scaled uniformly. The default alignment
is Alignment.Center
.
Content scale¶
ContentScale.Inside |
ContentScale.Crop |
Images are scaled using ContentScale.Fit
by default, but can be customized. A visual guide of all possible values can be found here.
Unlike Image()
, ZoomableImage
can pan images even when they're cropped. This can be useful for applications like wallpaper apps that may want to use ContentScale.Crop
to ensure that images always fill the screen.
Warning
Placeholders are visually incompatible with ContentScale.Inside
.
Click listeners¶
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.
The default behavior of toggling between minimum and maximum zoom levels on double-clicks can be overridden by using the onDoubleClick
parameter:
Keyboard shortcuts¶
ZoomableImage()
can observe keyboard and mouse shortcuts for panning and zooming when it is focused, either by the user or using 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()
}
By default, the following shortcuts are recognized. These can be customized (or disabled) by passing a custom HardwareShortcutsSpec
to rememberZoomableState()
.
Android | |
---|---|
Zoom in | Control + = |
Zoom out | Control + - |
Pan | Arrow keys |
Extra pan | Alt + arrow keys |
Sharing hoisted state¶
For handling zoom gestures, Zoomablemage
uses Modifier.zoomable()
underneath. If your app displays different kinds of media, it is recommended to hoist the ZoomableState
outside so that it can be shared with all zoomable composables: