Image: play with a ZoomableBox
This commit is contained in:
parent
c4226c25d8
commit
d6d693ff1f
2 changed files with 122 additions and 3 deletions
|
|
@ -17,16 +17,28 @@
|
||||||
package io.element.android.features.messages.impl.media.viewer
|
package io.element.android.features.messages.impl.media.viewer
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.gestures.awaitEachGesture
|
||||||
|
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||||
|
import androidx.compose.foundation.gestures.calculatePan
|
||||||
|
import androidx.compose.foundation.gestures.calculateZoom
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import io.element.android.features.messages.impl.media.viewer.model.MediaContentUiModel
|
import io.element.android.features.messages.impl.media.viewer.model.MediaContentUiModel
|
||||||
|
import io.element.android.libraries.designsystem.components.ZoomableBox
|
||||||
import io.element.android.libraries.designsystem.components.blurhash.BlurHashAsyncImage
|
import io.element.android.libraries.designsystem.components.blurhash.BlurHashAsyncImage
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
|
|
@ -54,14 +66,14 @@ private fun MediaImageViewer(
|
||||||
image: MediaContentUiModel.Image,
|
image: MediaContentUiModel.Image,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Box(
|
ZoomableBox(
|
||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize(),
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
) {
|
||||||
BlurHashAsyncImage(
|
BlurHashAsyncImage(
|
||||||
blurHash = image.blurhash,
|
blurHash = image.blurhash,
|
||||||
|
modifier = Modifier.fillMaxSize().zoomable(),
|
||||||
model = image.mediaRequestData,
|
model = image.mediaRequestData,
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Fit,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -79,6 +91,7 @@ private fun MediaVideoViewer(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun MediaViewerViewLightPreview(@PreviewParameter(MediaViewerStateProvider::class) state: MediaViewerState) =
|
fun MediaViewerViewLightPreview(@PreviewParameter(MediaViewerStateProvider::class) state: MediaViewerState) =
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.designsystem.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
|
import androidx.compose.foundation.gestures.detectTransformGestures
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
|
import androidx.compose.foundation.layout.LayoutScopeMarker
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
|
import androidx.compose.ui.unit.IntSize
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ZoomableBox(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
minScale: Float = 1f,
|
||||||
|
maxScale: Float = 5f,
|
||||||
|
content: @Composable ZoomableBoxScope.() -> Unit
|
||||||
|
) {
|
||||||
|
var scale by remember { mutableStateOf(1f) }
|
||||||
|
var offsetX by remember { mutableStateOf(0f) }
|
||||||
|
var offsetY by remember { mutableStateOf(0f) }
|
||||||
|
var size by remember { mutableStateOf(IntSize.Zero) }
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.clip(RectangleShape)
|
||||||
|
.onSizeChanged { size = it }
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectTransformGestures { _, pan, zoom, _ ->
|
||||||
|
scale = maxOf(minScale, minOf(scale * zoom, maxScale))
|
||||||
|
val maxX = (size.width * (scale - 1)) / 2
|
||||||
|
val minX = -maxX
|
||||||
|
offsetX = maxOf(minX, minOf(maxX, offsetX + pan.x))
|
||||||
|
val maxY = (size.height * (scale - 1)) / 2
|
||||||
|
val minY = -maxY
|
||||||
|
offsetY = maxOf(minY, minOf(maxY, offsetY + pan.y))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures(
|
||||||
|
onDoubleTap = {
|
||||||
|
offsetX = 0f
|
||||||
|
offsetY = 0f
|
||||||
|
scale = if (scale > minScale) {
|
||||||
|
minScale
|
||||||
|
} else {
|
||||||
|
maxScale / 2f
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
DefaultZoomableBoxScope(this, scale, offsetX, offsetY).content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@LayoutScopeMarker
|
||||||
|
@Immutable
|
||||||
|
interface ZoomableBoxScope : BoxScope {
|
||||||
|
@Stable
|
||||||
|
fun Modifier.zoomable(): Modifier
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DefaultZoomableBoxScope(
|
||||||
|
private val parentScope: BoxScope,
|
||||||
|
private val scale: Float,
|
||||||
|
private val offsetX: Float,
|
||||||
|
private val offsetY: Float
|
||||||
|
) : ZoomableBoxScope, BoxScope by parentScope {
|
||||||
|
|
||||||
|
override fun Modifier.zoomable(): Modifier {
|
||||||
|
return graphicsLayer(
|
||||||
|
scaleX = scale,
|
||||||
|
scaleY = scale,
|
||||||
|
translationX = offsetX,
|
||||||
|
translationY = offsetY
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue