commonMain.me.saket.telephoto.zoomable.ZoomableState.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of zoomable-desktop Show documentation
Show all versions of zoomable-desktop Show documentation
A Modifier for making anything zoomable.
The newest version!
@file:Suppress("DeprecatedCallableAddReplaceWith")
package me.saket.telephoto.zoomable
import androidx.annotation.FloatRange
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.SnapSpec
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalLayoutDirection
/**
* Create a [ZoomableState] that can be used with [Modifier.zoomable].
*
* @param zoomSpec See [ZoomSpec.maxZoomFactor] and [ZoomSpec.preventOverOrUnderZoom].
*
* @param autoApplyTransformations Determines whether the resulting scale and translation of pan and zoom
* gestures should be automatically applied by [Modifier.zoomable] to its content. This can be disabled
* if your content prefers applying the transformations in a bespoke manner.
*
* @param hardwareShortcutsSpec Spec used for handling keyboard and mouse shortcuts, or
* [HardwareShortcutsSpec.Disabled] for disabling them.
*/
@Composable
fun rememberZoomableState(
zoomSpec: ZoomSpec = ZoomSpec(),
autoApplyTransformations: Boolean = true,
hardwareShortcutsSpec: HardwareShortcutsSpec = HardwareShortcutsSpec(),
): ZoomableState {
return rememberSaveable(saver = RealZoomableState.Saver) {
RealZoomableState(
autoApplyTransformations = autoApplyTransformations,
)
}.also {
it.zoomSpec = zoomSpec
it.hardwareShortcutsSpec = hardwareShortcutsSpec
it.layoutDirection = LocalLayoutDirection.current
}
}
@Stable
sealed interface ZoomableState {
/**
* Transformations that should be applied to [Modifier.zoomable]'s content.
*
* See [ZoomableContentTransformation].
*/
val contentTransformation: ZoomableContentTransformation
/**
* Determines whether the resulting scale and translation of pan and zoom gestures
* should be automatically applied to by [Modifier.zoomable] to its content. This can
* be disabled if your content prefers applying the transformations in a bespoke manner.
* */
var autoApplyTransformations: Boolean
/**
* Single source of truth for your content's aspect ratio. If you're using `Modifier.zoomable()`
* with `Image()` or other composables that also accept [ContentScale], they should not be used
* to avoid any conflicts.
*
* A visual guide of the various scale values can be found
* [here](https://developer.android.com/jetpack/compose/graphics/images/customize#content-scale).
*/
var contentScale: ContentScale
/**
* Alignment of the content.
*
* When the content is zoomed, it is scaled with respect to this alignment until it
* is large enough to fill all available space. After that, they're scaled uniformly.
* */
var contentAlignment: Alignment
/**
* The visual bounds of the content, calculated by applying the scale and translation of pan and zoom
* gestures to the value given to [ZoomableState.setContentLocation]. Useful for drawing decorations
* around the content or performing hit tests.
*/
val transformedContentBounds: Rect
/**
* The content's current zoom as a fraction of its min and max allowed zoom factors.
*
* @return A value between 0 and 1, where 0 indicates that the content is fully zoomed out,
* 1 indicates that the content is fully zoomed in, and `null` indicates that an initial zoom
* value hasn't been calculated yet and the content is hidden. A `null` value could be safely
* treated the same as 0, but [Modifier.zoomable] leaves that decision up to you.
*/
@get:FloatRange(from = 0.0, to = 1.0)
val zoomFraction: Float?
/** The zoom spec passed to [rememberZoomableState]. */
val zoomSpec: ZoomSpec
/** See [ZoomableContentLocation]. */
fun setContentLocation(location: ZoomableContentLocation)
/**
* Reset content to its minimum zoom and zero offset and suspend until it's finished.
*
* @param animationSpec The animation spec to use or [SnapSpec] for no animation.
*/
suspend fun resetZoom(animationSpec: AnimationSpec = DefaultZoomAnimationSpec)
/**
* Zooms in or out around [centroid] by a ratio of [zoomFactor] relative to the current size,
* and suspends until it's finished.
*
* @param zoomFactor Ratio by which to zoom relative to the current size. For example, a [zoomFactor]
* of `3f` will triple the *current* zoom level.
*
* @param centroid Focal point for this zoom within the content's size. Defaults to the center
* of the content.
*
* @param animationSpec The animation spec to use or [SnapSpec] for no animation.
*/
suspend fun zoomBy(
zoomFactor: Float,
centroid: Offset = Offset.Unspecified,
animationSpec: AnimationSpec = DefaultZoomAnimationSpec,
)
/**
* Zooms in or out around [centroid] to achieve a final zoom level specified by [zoomFactor],
* and suspends until it's finished.
*
* @param zoomFactor Target zoom level for the content. For example, a [zoomFactor] of `2f` will
* set the content's zoom level to two times its *original* size. This value is internally coerced
* to at most [ZoomSpec.maxZoomFactor].
*
* @param centroid Focal point for this zoom within the content's size. Defaults to the center
* of the content.
*
* @param animationSpec The animation spec to use or [SnapSpec] for no animation.
*/
suspend fun zoomTo(
zoomFactor: Float,
centroid: Offset = Offset.Unspecified,
animationSpec: AnimationSpec = DefaultZoomAnimationSpec,
)
/**
* Animate pan by [offset] Offset in pixels and suspend until it's finished.
*
* @param animationSpec The animation spec to use or [SnapSpec] for no animation.
*/
suspend fun panBy(
offset: Offset,
animationSpec: AnimationSpec = DefaultPanAnimationSpec,
)
/**
* Reset content to its minimum zoom and zero offset and suspend until it's finished.
*/
@Deprecated(message = "Use resetZoom(AnimationSpec) instead")
suspend fun resetZoom(withAnimation: Boolean) {
if (withAnimation) {
resetZoom()
} else {
resetZoom(animationSpec = SnapSpec())
}
}
/** See [ZoomableContentLocation]. */
@Deprecated(
message = "Use setContentLocation() instead",
replaceWith = ReplaceWith("setContentLocation"),
level = DeprecationLevel.HIDDEN,
)
@Suppress("INAPPLICABLE_JVM_NAME") // https://youtrack.jetbrains.com/issue/KT-31420
@JvmName("setContentLocation")
suspend fun setContentLocationSuspending(location: ZoomableContentLocation) {
setContentLocation(location)
}
companion object {
val DefaultZoomAnimationSpec: AnimationSpec get() = spring(stiffness = Spring.StiffnessMediumLow)
val DefaultPanAnimationSpec: AnimationSpec get() = spring(stiffness = Spring.StiffnessMediumLow)
}
}
@Deprecated("Kept for binary compatibility", level = DeprecationLevel.HIDDEN)
@Composable
fun rememberZoomableState(
zoomSpec: ZoomSpec = ZoomSpec(),
autoApplyTransformations: Boolean = true,
): ZoomableState = rememberZoomableState(
zoomSpec = zoomSpec,
autoApplyTransformations = autoApplyTransformations,
hardwareShortcutsSpec = HardwareShortcutsSpec(),
)
© 2015 - 2024 Weber Informatics LLC | Privacy Policy