Skip to content

Modifier.zoomable()

A Modifier for handling pan & zoom gestures, designed to be shared across all your media composables so that your users can use the same familiar gestures throughout your app. It offers,

  • Pinch to zoom and flings
  • Double tap to zoom
  • Single finger zoom (double tap and hold)
  • Haptic feedback for over/under zoom
  • Compatibility with nested scrolling
  • Click listeners

Installation

implementation("me.saket.telephoto:zoomable:0.10.0")
Box(
  Modifier
    .size(200.dp)
    .zoomable(rememberZoomableState())
    .background(
      brush = Brush.linearGradient(listOf(Color.Cyan, Color.Blue)),
      shape = RoundedCornerShape(4.dp)
    )
)

While Modifier.zoomable() was primarily written with images & videos in mind, it can be used for anything such as text, canvas drawings, etc.

Edge detection

Without edge detection With edge detection

For preventing your content from over-zooming or over-panning, Modifier.zoomable() will use your content's layout size by default. This is good enough for composables that fill every pixel of their drawing space.

For richer content such as an Image() whose visual size may not always match its layout size, Modifier.zoomable() will need your assistance.

val state = rememberZoomableState()
val painter = resourcePainter(R.drawable.example)

LaunchedEffect(painter.intrinsicSize) {
  state.setContentLocation(
    ZoomableContentLocation.scaledInsideAndCenterAligned(painter.intrinsicSize)
  )
}

Image(
  modifier = Modifier
    .fillMaxSize()
    .background(Color.Orange)
    .zoomable(state),
  painter = painter,
  contentDescription = ,
  contentScale = ContentScale.Inside,
  alignment = Alignment.Center,
)

Click listeners

For detecting double taps, Modifier.zoomable() consumes all tap gestures making it incompatible with Modifier.clickable() and Modifier.combinedClickable(). As an alternative, its onClick and onLongClick parameters can be used.

Modifier.zoomable(
  state = rememberZoomableState(),
  onClick = {  },
  onLongClick = {  },
)

Applying gesture transformations

When pan & zoom gestures are received, Modifier.zoomable() automatically applies their resulting scale and translation onto your content using Modifier.graphicsLayer().

This can be disabled if your content prefers applying the transformations in a bespoke manner.

val state = rememberZoomableState(
  autoApplyTransformations = false
)

Text(
  modifier = Modifier
    .fillMaxSize()
    .zoomable(state),
  text = "Nicolas Cage",
  style = state.contentTransformation.let {
    val weightMultiplier = if (it.isUnspecified) 1f else it.scale.scaleX
    TextStyle(
      fontSize = 36.sp,
      fontWeight = FontWeight(400 * weightMultiplier),
    )
  }
)