I was making a fullscreen photo viewer which contain a pager (used HorizontalPager
) and each page, user can zoom in/out and pan the image, but still able to swipe through pages.
My idea is swiping page will occurs when the image is not zoomed in (scale factor = 1), if it's zoomed in (scale factor > 1) then dragging/swiping will pan the image around.
Here is the code for the HorizontalPager
that contain my customized zoomable Image:
@ExperimentalPagerApi
@Composable
fun ViewPagerSlider(pagerState: PagerState, urls: List<String>) {
var scale = remember {
mutableStateOf(1f)
}
var transX = remember {
mutableStateOf(0f)
}
var transY = remember {
mutableStateOf(0f)
}
HorizontalPager(
count = urls.size,
state = pagerState,
modifier = Modifier
.padding(0.dp, 40.dp, 0.dp, 40.dp),
) { page ->
Image(
painter = rememberImagePainter(
data = urls[page],
emptyPlaceholder = R.drawable.img_default_post,
),
contentScale = ContentScale.FillHeight,
contentDescription = null,
modifier = Modifier
.fillMaxSize()
.graphicsLayer(
translationX = transX.value,
translationY = transY.value,
scaleX = scale.value,
scaleY = scale.value,
)
.pointerInput(scale.value) {
detectTransformGestures { _, pan, zoom, _ ->
scale.value = when {
scale.value < 1f -> 1f
scale.value > 3f -> 3f
else -> scale.value * zoom
}
if (scale.value > 1f) {
transX.value = transX.value + (pan.x * scale.value)
transY.value = transY.value + (pan.y * scale.value)
} else {
transX.value = 0f
transY.value = 0f
}
}
}
)
}
}
So my image is zoomed in maximum 3f, and cannot zoom out smaller than 0.
I cannot swipe to change to another page if detectTransformGestures
is in my code. If I put the detectTransformGestures
based on the factor (scale = 1, make it swipeable to another page if not zoomed in), then it will be a "deadlock" as I cannot zoom in because there is no listener.
I don't know if there is some how to make it possible...
Thank you guys for your time!
I had to do something similar, and came up with this:
private fun ZoomableImage(
modifier: Modifier = Modifier,
bitmap: ImageBitmap,
maxScale: Float = 1f,
minScale: Float = 3f,
contentScale: ContentScale = ContentScale.Fit,
isRotation: Boolean = false,
isZoomable: Boolean = true,
lazyState: LazyListState
) {
val scale = remember { mutableStateOf(1f) }
val rotationState = remember { mutableStateOf(1f) }
val offsetX = remember { mutableStateOf(1f) }
val offsetY = remember { mutableStateOf(1f) }
val coroutineScope = rememberCoroutineScope()
Box(
modifier = Modifier
.clip(RectangleShape)
.background(Color.Transparent)
.combinedClickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = { /* NADA :) */ },
onDoubleClick = {
if (scale.value >= 2f) {
scale.value = 1f
offsetX.value = 1f
offsetY.value = 1f
} else scale.value = 3f
},
)
.pointerInput(Unit) {
if (isZoomable) {
forEachGesture {
awaitPointerEventScope {
awaitFirstDown()
do {
val event = awaitPointerEvent()
scale.value *= event.calculateZoom()
if (scale.value > 1) {
coroutineScope.launch {
lazyState.setScrolling(false)
}
val offset = event.calculatePan()
offsetX.value += offset.x
offsetY.value += offset.y
rotationState.value += event.calculateRotation()
coroutineScope.launch {
lazyState.setScrolling(true)
}
} else {
scale.value = 1f
offsetX.value = 1f
offsetY.value = 1f
}
} while (event.changes.any { it.pressed })
}
}
}
}
) {
Image(
bitmap = bitmap,
contentDescription = null,
contentScale = contentScale,
modifier = modifier
.align(Alignment.Center)
.graphicsLayer {
if (isZoomable) {
scaleX = maxOf(maxScale, minOf(minScale, scale.value))
scaleY = maxOf(maxScale, minOf(minScale, scale.value))
if (isRotation) {
rotationZ = rotationState.value
}
translationX = offsetX.value
translationY = offsetY.value
}
}
)
}
}
It is zoomable, rotatable (if you want it), supports pan if the image is zoomed in, has support for double-click zoom-in and zoom-out and also supports being used inside a scrollable element. I haven't come up with a solution to limit how far can the user pan the image yet.
It uses combinedClickable
so the double-click zoom works without interfering with the other gestures, and pointerInput
for the zoom, pan and rotation.
It uses this extension function to control the LazyListState
, but if you need it for ScrollState
it shouldn't be hard to modify it to suit your needs:
suspend fun LazyListState.setScrolling(value: Boolean) {
scroll(scrollPriority = MutatePriority.PreventUserInput) {
when (value) {
true -> Unit
else -> awaitCancellation()
}
}
}
Feel free to modify it for your needs.
Solution for HorizontalPager with better ux on swipe:
val pagerState = rememberPagerState()
val scrollEnabled = remember { mutableStateOf(true) }
HorizontalPager(
count = ,
state = pagerState,
userScrollEnabled = scrollEnabled.value,
) { }
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ZoomablePagerImage(
modifier: Modifier = Modifier,
painter: Painter,
scrollEnabled: MutableState<Boolean>,
minScale: Float = 1f,
maxScale: Float = 5f,
contentScale: ContentScale = ContentScale.Fit,
isRotation: Boolean = false,
) {
var targetScale by remember { mutableStateOf(1f) }
val scale = animateFloatAsState(targetValue = maxOf(minScale, minOf(maxScale, targetScale)))
var rotationState by remember { mutableStateOf(1f) }
var offsetX by remember { mutableStateOf(1f) }
var offsetY by remember { mutableStateOf(1f) }
val configuration = LocalConfiguration.current
val screenWidthPx = with(LocalDensity.current) { configuration.screenWidthDp.dp.toPx() }
Box(
modifier = Modifier
.clip(RectangleShape)
.background(Color.Transparent)
.combinedClickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = { },
onDoubleClick = {
if (targetScale >= 2f) {
targetScale = 1f
offsetX = 1f
offsetY = 1f
scrollEnabled.value = true
} else targetScale = 3f
},
)
.pointerInput(Unit) {
forEachGesture {
awaitPointerEventScope {
awaitFirstDown()
do {
val event = awaitPointerEvent()
val zoom = event.calculateZoom()
targetScale *= zoom
val offset = event.calculatePan()
if (targetScale <= 1) {
offsetX = 1f
offsetY = 1f
targetScale = 1f
scrollEnabled.value = true
} else {
offsetX += offset.x
offsetY += offset.y
if (zoom > 1) {
scrollEnabled.value = false
rotationState += event.calculateRotation()
}
val imageWidth = screenWidthPx * scale.value
val borderReached = imageWidth - screenWidthPx - 2 * abs(offsetX)
scrollEnabled.value = borderReached <= 0
if (borderReached < 0) {
offsetX = ((imageWidth - screenWidthPx) / 2f).withSign(offsetX)
if (offset.x != 0f) offsetY -= offset.y
}
}
} while (event.changes.any { it.pressed })
}
}
}
) {
Image(
painter = painter,
contentDescription = null,
contentScale = contentScale,
modifier = modifier
.align(Alignment.Center)
.graphicsLayer {
this.scaleX = scale.value
this.scaleY = scale.value
if (isRotation) {
rotationZ = rotationState
}
this.translationX = offsetX
this.translationY = offsetY
}
)
}
}
If you can create a mutable state variable that keeps track of the zoom factor, you can add the pointerInput modifier when the zoom factor is greater than one and leave it out when it is greater than one. Something like this:
var zoomFactorGreaterThanOne by remember { mutableStateOf(false) }
Image(
painter = rememberImagePainter(
data = urls[page],
emptyPlaceholder = R.drawable.img_default_post,
),
contentScale = ContentScale.FillHeight,
contentDescription = null,
modifier = Modifier
.fillMaxSize()
.graphicsLayer(
translationX = transX.value,
translationY = transY.value,
scaleX = scale.value,
scaleY = scale.value,
)
.run {
if (zoomFactorGreaterThanOne != 1.0f) {
this.pointerInput(scale.value) {
detectTransformGestures { _, pan, zoom, _ ->
zoomFactorGreaterThanOne = scale.value > 1
scale.value = when {
scale.value < 1f -> 1f
scale.value > 3f -> 3f
else -> scale.value * zoom
}
if (scale.value > 1f) {
transX.value = transX.value + (pan.x * scale.value)
transY.value = transY.value + (pan.y * scale.value)
} else {
transX.value = 0f
transY.value = 0f
}
}
}
} else {
this
}
}
)
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.