我正在制作一个全屏照片查看器,其中包含一个寻呼机(使用
HorizontalPager
)和每个页面,用户可以放大/缩小和平移图像,但仍然可以滑动页面。
我的想法是,当图像未放大(比例因子= 1)时,会发生滑动页面,如果放大(比例因子> 1),则拖动/滑动将平移图像。
这是包含我的自定义可缩放图像的
HorizontalPager
的代码:
@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
}
}
}
)
}
}
所以我的图像最大放大了3f,并且不能缩小到小于0。
如果
detectTransformGestures
在我的代码中,我无法滑动以更改到另一个页面。如果我根据因子放置detectTransformGestures
(scale = 1,如果不放大则可以滑动到另一个页面),那么这将是一个“死锁”,因为我无法放大,因为没有侦听器。
我不知道是否有一些方法可以实现...
谢谢大家的宝贵时间!
我必须做类似的事情,并想出了这个:
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
}
}
)
}
}
它是可缩放的,可旋转的(如果你想要的话),如果图像放大则支持平移,支持双击放大和缩小,还支持在可滚动元素内使用。我还没有想出一个解决方案来限制用户可以平移图像多远。
它使用
combinedClickable
进行双击缩放,而不会干扰其他手势,并使用 pointerInput
进行缩放、平移和旋转。
它使用此扩展功能来控制
LazyListState
,但如果您需要它ScrollState
,那么修改它以满足您的需求应该不难:
suspend fun LazyListState.setScrolling(value: Boolean) {
scroll(scrollPriority = MutatePriority.PreventUserInput) {
when (value) {
true -> Unit
else -> awaitCancellation()
}
}
}
请随意根据您的需要进行修改。
HorizontalPager 的解决方案,在滑动时具有更好的用户体验:
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
}
)
}
}
如果您可以创建一个可变状态变量来跟踪缩放系数,则可以在缩放系数大于 1 时添加pointerInput 修饰符,并在缩放系数大于 1 时将其保留。像这样的东西:
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
}
}
)
修复了之前answer存在的错误
原创:Android 画廊
修改器的扩展,支持点击和手势操作,而不会在像 HorizontalPager 这样的可滚动容器中相互重叠 在 HorizontalPager 的情况下,如果我们消耗所有手势,那么 Pager 的滑动手势也将被忽略;另一方面,如果我们不使用任何手势,就可以解决之前的问题,但会发现一个新问题:变换和点击手势都是重叠的,当用户想要缩放然后平移时,这会带来非常糟糕的用户体验图像,点击/双击手势也被注册
!(scrollEnabled 逻辑值更改应针对每种情况手动实现)
fun Modifier.tapAndGesture(
key: Any? = Unit,
onTap: ((Offset) -> Unit)? = null,
onDoubleTap: ((Offset) -> Unit)? = null,
onGesture: ((centeroid: Offset, pan: Offset, zoom: Float, rotation: Float) -> Unit)? = null,
scrollEnabled: MutableState<Boolean> = mutableStateOf(false)
) = composed(
factory = {
val scope = rememberCoroutineScope()
val gestureModifier = Modifier.pointerInput(key) {
/**
* [detectTransformGestures]
* @author SmartToolFactory
* href: https://github.com/SmartToolFactory/Compose-Extended-Gestures
*/
detectTransformGestures(
consume = false,
onGesture = { centroid: Offset,
pan: Offset,
zoom: Float,
rotation: Float,
_: PointerInputChange,
changes: List<PointerInputChange> ->
scope.launch {
onGesture?.invoke(centroid, pan, zoom, rotation)
}
changes.forEach {
// Consume if scroll gestures are not possible
if (!scrollEnabled.value) it.consume()
}
}
)
}
val tapModifier = Modifier.pointerInput(key) {
detectTapGestures(
onDoubleTap = onDoubleTap,
onTap = onTap
)
}
then(gestureModifier.then(tapModifier))
},
inspectorInfo = debugInspectorInfo {
name = "tapAndGesture"
properties["key"] = key
properties["onTap"] = onTap
properties["onDoubleTap"] = onDoubleTap
properties["onGesture"] = onGesture
}
)
作为一个选项,还可以在 HorizontalPager 本身中处理滑动和缩放事件。
@Composable
fun PicturesPager(
urls: List<String>,
contentScale: ContentScale = ContentScale.Fit,
modifier: Modifier = Modifier
) {
val pagerState = rememberPagerState(pageCount = { urls.size })
val canSwipe = remember { mutableStateOf(true) }
var scale by remember { mutableStateOf(1f) }
val offset = remember { mutableStateOf(Offset.Zero) }
val size = remember { mutableStateOf(Size.Zero) }
HorizontalPager(
state = pagerState, modifier = modifier
.pointerInput(Unit) {
detectTransformGestures { _, pan, zoom, _ ->
val newScale = scale * zoom
scale = newScale.coerceAtLeast(1f)
// you can set value to == 1 but it's not always easy for the
// user to zoom out image exactly to 1 so personally I
// find setting it to 1.05 more convenient
canSwipe.value = scale < 1.05f
val maxOffsetX = (size.value.width * scale - size.value.width) / 2
val newOffsetX =
(offset.value.x + pan.x).coerceIn(-maxOffsetX, maxOffsetX)
val maxOffsetY = (size.value.height * scale - size.value.height) / 2
val newOffsetY =
(offset.value.y + pan.y).coerceIn(-maxOffsetY, maxOffsetY)
offset.value = Offset(newOffsetX, newOffsetY)
}
// enable/disable page swipe based on scale factor
}, userScrollEnabled = canSwipe.value
) { page ->
ZoomableImage(
imageUrl = urls[page],
scale = scale,
offset = offset,
size = size,
contentScale = contentScale
)
}
}
@OptIn(ExperimentalResourceApi::class)
@Composable
fun ZoomableImage(
imageUrl: String,
scale: Float,
offset: MutableState<Offset>,
size: MutableState<Size>,
contentScale: ContentScale,
) {
//The box here is not a requirement so you can remove it and set graphicsLayer to the image directly
Box(
modifier = Modifier
.graphicsLayer(
scaleX = maxOf(1f, scale),
scaleY = maxOf(1f, scale),
translationX = offset.value.x,
translationY = offset.value.y
)
) {
KamelImage(
resource = asyncPainterResource(imageUrl),
contentDescription = stringResource(Res.string.hint_auction_image),
contentScale = contentScale,
// Here we are getting an updated image size and passing it back to HorizontalPager
modifier = Modifier.fillMaxWidth().onSizeChanged { newSize ->
size.value = Size(newSize.width.toFloat(), newSize.height.toFloat())
},
onLoading = { _ ->
CircularProgressIndicator()
}
)
}
}